@wix/agent-skills 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +14 -112
  3. package/skills/wix-app/references/APP_TOOLS.md +2 -1
  4. package/skills/wix-app/references/BACKEND_EVENT.md +3 -3
  5. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +41 -152
  6. package/skills/wix-app/references/DASHBOARD_MODAL.md +4 -4
  7. package/skills/wix-app/references/SDK_MODULE_MAP.md +44 -0
  8. package/skills/wix-app/references/SITE_PLUGIN.md +36 -190
  9. package/skills/wix-app/references/app-tools/TOOLS.md +3 -1
  10. package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +21 -0
  11. package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +17 -0
  12. package/skills/wix-app/references/dashboard-plugin/SLOTS.md +4 -18
  13. package/skills/wix-app/references/site-plugin/EXAMPLES.md +1 -1
  14. package/skills/wix-headless-fast/SKILL.md +302 -51
  15. package/skills/wix-headless-fast/entry/skill.md +11 -2
  16. package/skills/wix-headless-fast/install/AGENTS.md +32 -0
  17. package/skills/wix-headless-fast/install/agents-md.mjs +39 -0
  18. package/skills/wix-headless-fast/install/attach.mjs +360 -0
  19. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  20. package/skills/wix-headless-fast/install/setup.mjs +239 -0
  21. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +226 -60
  22. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  23. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  24. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  25. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  26. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  27. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  28. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  29. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  30. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  31. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  32. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  33. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  34. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  35. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  36. package/skills/wix-headless-fast/references/blog/seed/read-site.mjs +22 -0
  37. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +181 -33
  38. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  39. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  40. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  41. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  42. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  43. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  44. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  45. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  46. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  47. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  48. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  49. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  50. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  51. package/skills/wix-headless-fast/references/bookings/seed/read-site.mjs +28 -0
  52. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +238 -62
  53. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  54. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  55. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  56. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  57. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  58. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  59. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  60. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  61. package/skills/wix-headless-fast/references/cms/seed/read-site.mjs +20 -0
  62. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +241 -83
  63. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  64. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  65. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  66. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  67. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  68. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  69. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  70. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  71. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  72. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  73. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  74. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  75. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  76. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  77. package/skills/wix-headless-fast/references/events/seed/read-site.mjs +26 -0
  78. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +216 -26
  79. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  80. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  81. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  82. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  83. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  84. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  85. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  86. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  87. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  88. package/skills/wix-headless-fast/references/forms/seed/read-site.mjs +20 -0
  89. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  90. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +316 -63
  91. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  92. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  93. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  94. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  95. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  96. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  97. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  98. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  99. package/skills/wix-headless-fast/references/members/seed/read-site.mjs +15 -0
  100. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +240 -59
  101. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  102. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  103. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  104. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  105. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  106. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  107. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  108. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  109. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  110. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  111. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  112. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  113. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  114. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  115. package/skills/wix-headless-fast/references/portfolio/seed/read-site.mjs +20 -0
  116. package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +19 -21
  117. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +214 -51
  118. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  119. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  120. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  121. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  122. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  123. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  124. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  125. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  126. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  127. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  128. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  129. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  130. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  131. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  132. package/skills/wix-headless-fast/references/pricing-plans/seed/read-site.mjs +21 -0
  133. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +242 -47
  134. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  135. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  136. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  137. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  138. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  139. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  140. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  141. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  142. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  143. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  144. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  145. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  146. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  147. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  148. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  149. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  150. package/skills/wix-headless-fast/references/restaurants/seed/read-site.mjs +25 -0
  151. package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +25 -5
  152. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  153. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  154. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  155. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  156. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  157. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  158. package/skills/wix-headless-fast/references/shared/seed/read-site.mjs +99 -0
  159. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +186 -63
  160. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  161. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  162. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  163. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  164. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  165. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  166. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +314 -0
  167. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  168. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  169. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  170. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +2 -0
  171. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  172. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  173. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +31 -9
  174. package/skills/wix-headless-fast/references/storefront/seed/read-site.mjs +38 -0
  175. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +57 -50
  176. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  177. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-coupon.md +10 -19
  178. package/skills/wix-manage/references/events/create-wix-event.md +5 -1
  179. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  180. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  181. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  182. package/skills/wix-headless-fast/install/fast-path.mjs +0 -186
  183. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -0,0 +1,92 @@
1
+ // Service rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./services.ts
2
+ // (the SDK, managed Astro and React) and the REST twin in references/bookings/rest/services.ts
3
+ // (fetch, a static site or a port to another language). Every rule about price, "Free", duration,
4
+ // media, staff, and the payment option lives HERE, once. A raw entity may come from the SDK (`_id`,
5
+ // media as a `wix:image://` string) or from REST (`id`, media as an { id, url, width, height }
6
+ // object); the mappers accept both. Imports are type-only so a strip to JS emits no imports.
7
+ import type { BookingCategory, ServiceDetail, ServiceSummary, ServiceType } from "./types";
8
+
9
+ /** A raw Services V2 entity as either transport returns it. */
10
+ export type Raw = Record<string, any>;
11
+ /** Media value + size → https URL. Injected: the SDK transport scales through @wix/sdk, REST through the URL form. */
12
+ export type ImgSrc = (value: any, width: number, height: number) => string;
13
+
14
+ /** The Wix Bookings app id — the cart's catalogReference.appId and the services filter. */
15
+ export const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
16
+ /** Staff-member resource type id (ANY_RESOURCE fallback + staff filtering). */
17
+ export const STAFF_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155";
18
+ /** Requested on every service read — without it `staffMemberDetails` is absent and the DTO's staff is []. */
19
+ export const SERVICES_CONDITIONAL_FIELDS = ["STAFF_MEMBER_DETAILS"] as const;
20
+
21
+ export const rawId = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
22
+
23
+ /** The filter every services read shares: this app's services, optionally one slug (`mainSlug.name`). */
24
+ export function servicesFilter(slug?: string): Raw {
25
+ return { appId: BOOKINGS_APP_ID, ...(slug ? { "mainSlug.name": slug } : {}) };
26
+ }
27
+
28
+ /** Hidden services never reach a tile — the same rule on both transports. */
29
+ export const isVisible = (raw: Raw): boolean => !raw.hidden;
30
+
31
+ /**
32
+ * A media value the image helpers can resolve. The SDK hands over a `wix:image://v1/<id>/<name>#…`
33
+ * string (or an https URL); REST hands over the image OBJECT with a bare file id — rebuilt into the
34
+ * same `wix:image://` form the SDK produces, so imgSrc scales both identically.
35
+ */
36
+ export function mediaValue(image: unknown): unknown {
37
+ if (!image || typeof image === "string") return image;
38
+ const o = image as Raw;
39
+ const url: string = o.url ?? "";
40
+ if (/^(https?:|wix:image:)/.test(url)) return url;
41
+ if (o.id) return `wix:image://v1/${o.id}/${encodeURIComponent(o.filename ?? "")}#originWidth=${o.width ?? 0}&originHeight=${o.height ?? 0}`;
42
+ return url;
43
+ }
44
+
45
+ export function formatPrice(value: string | undefined, currency: string | undefined): string {
46
+ if (value == null || value === "" || Number(value) === 0) return "Free";
47
+ try {
48
+ return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD" }).format(Number(value));
49
+ } catch {
50
+ return `${value} ${currency ?? ""}`.trim();
51
+ }
52
+ }
53
+
54
+ export function toSummary(raw: Raw, imgSrc: ImgSrc): ServiceSummary {
55
+ const price = raw.payment?.fixed?.price;
56
+ const free = raw.payment?.rateType === "NO_FEE" || !price?.value || Number(price.value) === 0;
57
+ return {
58
+ id: rawId(raw),
59
+ slug: raw.mainSlug?.name ?? raw.supportedSlugs?.[0]?.name ?? "",
60
+ name: raw.name ?? "",
61
+ tagLine: raw.tagLine ?? "",
62
+ type: (raw.type ?? "APPOINTMENT") as ServiceType,
63
+ price: free ? "Free" : formatPrice(price?.value, price?.currency),
64
+ free,
65
+ durationMinutes: raw.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? null,
66
+ imageUrl: imgSrc(mediaValue(raw.media?.mainMedia?.image), 800, 800),
67
+ categoryId: rawId(raw.category) || null,
68
+ staff: ((raw.staffMemberDetails?.staffMembers ?? []) as Raw[])
69
+ .map((m) => ({ id: m.staffMemberId ?? "", name: m.name ?? "" }))
70
+ .filter((m) => m.id),
71
+ };
72
+ }
73
+
74
+ export function toDetail(raw: Raw, imgSrc: ImgSrc): ServiceDetail {
75
+ const summary = toSummary(raw, imgSrc);
76
+ const online = raw.payment?.options?.online === true;
77
+ const inPerson = raw.payment?.options?.inPerson === true;
78
+ // Derive — never hardcode "ONLINE": a free/pay-in-person service booked ONLINE gets
79
+ // rejected by the cart with INSUFFICIENT_INVENTORY.
80
+ const paymentOption: "ONLINE" | "OFFLINE" = !online && inPerson ? "OFFLINE" : "ONLINE";
81
+ return {
82
+ ...summary,
83
+ description: raw.description ?? "",
84
+ formId: rawId(raw.form) || null,
85
+ paymentOption,
86
+ cancellationFeeEnabled: raw.bookingPolicy?.cancellationFeePolicy?.enabled === true,
87
+ };
88
+ }
89
+
90
+ export function toCategory(raw: Raw): BookingCategory {
91
+ return { id: rawId(raw), name: raw.name ?? "" };
92
+ }
@@ -0,0 +1,90 @@
1
+ // The services listing as a framework-free store — the logic behind useServices, usable from React
2
+ // (useServices wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and
3
+ // render), or as the specification for a port. State, actions, subscribe/getState, emit after every
4
+ // change. SSR-friendly: seed with `initialServices`/`initialCategories` and nothing is fetched;
5
+ // without them `start()` loads both. Category filtering is client-side — bookings catalogs are
6
+ // small and fully fetched. One store per mounted listing: createServicesStore(), not a singleton.
7
+ import { fetchBookingCategories, fetchServices } from "./services";
8
+ import type { BookingCategory, ServiceSummary } from "./types";
9
+
10
+ export interface ServicesStoreOptions {
11
+ initialServices?: ServiceSummary[];
12
+ initialCategories?: BookingCategory[];
13
+ }
14
+
15
+ /** Everything a listing surface renders from. Read it with getState() or through a subscription. */
16
+ export interface ServicesState {
17
+ /** null while the first load is in flight — render skeletons, not an empty state. */
18
+ services: ServiceSummary[] | null;
19
+ categories: BookingCategory[];
20
+ activeCategoryId: string | null;
21
+ error: string | null;
22
+ }
23
+
24
+ export interface ServicesStore {
25
+ getState(): ServicesState;
26
+ subscribe(listener: () => void): () => void;
27
+ /** Fetch whatever wasn't seeded. Call once when mounted (a browser). */
28
+ start(): void;
29
+ /** Stop reacting; drop late responses. */
30
+ stop(): void;
31
+ setActiveCategoryId(id: string | null): void;
32
+ }
33
+
34
+ export function createServicesStore({ initialServices, initialCategories }: ServicesStoreOptions = {}): ServicesStore {
35
+ let all: ServiceSummary[] | null = initialServices ?? null;
36
+ let categories: BookingCategory[] = initialCategories ?? [];
37
+ let activeCategoryId: string | null = null;
38
+ let error: string | null = null;
39
+ let started = false;
40
+ const listeners = new Set<() => void>();
41
+ let snapshot: ServicesState | null = null;
42
+ const emit = () => {
43
+ snapshot = null;
44
+ for (const fn of listeners) fn();
45
+ };
46
+
47
+ function getState(): ServicesState {
48
+ if (snapshot) return snapshot;
49
+ snapshot = {
50
+ services: all === null ? null : activeCategoryId ? all.filter((s) => s.categoryId === activeCategoryId) : all,
51
+ categories,
52
+ activeCategoryId,
53
+ error,
54
+ };
55
+ return snapshot;
56
+ }
57
+
58
+ return {
59
+ getState,
60
+ subscribe(fn) {
61
+ listeners.add(fn);
62
+ return () => listeners.delete(fn);
63
+ },
64
+ start() {
65
+ if (started) return;
66
+ started = true;
67
+ if (!initialServices) {
68
+ fetchServices()
69
+ .then((s) => { if (started) { all = s; emit(); } })
70
+ .catch((e) => {
71
+ if (!started) return;
72
+ all = [];
73
+ error = e instanceof Error ? e.message : String(e);
74
+ emit();
75
+ });
76
+ }
77
+ if (!initialCategories) {
78
+ fetchBookingCategories().then((c) => { if (started) { categories = c; emit(); } });
79
+ }
80
+ },
81
+ stop() {
82
+ started = false;
83
+ },
84
+ setActiveCategoryId(id) {
85
+ if (id === activeCategoryId) return;
86
+ activeCategoryId = id;
87
+ emit();
88
+ },
89
+ };
90
+ }
@@ -1,95 +1,56 @@
1
- // Service reads (Wix Bookings Services V2) — the only file that touches raw service
2
- // entities. Everything it returns is a plain DTO from ./types. Copy as-is; extend by adding
3
- // functions, not by editing these.
1
+ // Service reads (Wix Bookings Services V2) over the SDK — the only file that touches raw service
2
+ // entities on this transport. Everything it returns is a plain DTO from ./types. The rules and
3
+ // mappers live in ./services-core (shared with the REST twin in references/bookings/rest/); this
4
+ // file is the transport only. Copy as-is; extend by adding functions, not by editing these.
4
5
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/query-services.md
5
6
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/categories-v2/query-categories.md
6
7
  import { services as servicesModule, categoriesV2 } from "@wix/bookings";
7
8
  import { wixModule } from "../sdk";
8
9
  import { imgSrc } from "../media";
9
- import type { BookingCategory, ServiceDetail, ServiceSummary, ServiceType } from "./types";
10
+ import {
11
+ BOOKINGS_APP_ID,
12
+ SERVICES_CONDITIONAL_FIELDS,
13
+ STAFF_RESOURCE_TYPE_ID,
14
+ isVisible,
15
+ toCategory,
16
+ toDetail,
17
+ toSummary,
18
+ type Raw,
19
+ } from "./services-core";
20
+ import type { BookingCategory, ServiceDetail, ServiceSummary } from "./types";
21
+
22
+ export { BOOKINGS_APP_ID, STAFF_RESOURCE_TYPE_ID };
10
23
 
11
24
  const services = wixModule(servicesModule);
12
25
  const categories = wixModule(categoriesV2);
13
26
 
14
- /** The Wix Bookings app id — the cart's catalogReference.appId and the services filter. */
15
- export const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
16
- /** Staff-member resource type id (ANY_RESOURCE fallback + staff filtering). */
17
- export const STAFF_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155";
18
-
19
- type Raw = Record<string, any>;
20
-
21
- function formatPrice(value: string | undefined, currency: string | undefined): string {
22
- if (value == null || value === "" || Number(value) === 0) return "Free";
23
- try {
24
- return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD" }).format(Number(value));
25
- } catch {
26
- return `${value} ${currency ?? ""}`.trim();
27
- }
28
- }
29
-
30
- function toSummary(raw: Raw): ServiceSummary {
31
- const price = raw.payment?.fixed?.price;
32
- const free = raw.payment?.rateType === "NO_FEE" || !price?.value || Number(price.value) === 0;
33
- return {
34
- id: raw._id ?? "",
35
- slug: raw.mainSlug?.name ?? raw.supportedSlugs?.[0]?.name ?? "",
36
- name: raw.name ?? "",
37
- tagLine: raw.tagLine ?? "",
38
- type: (raw.type ?? "APPOINTMENT") as ServiceType,
39
- price: free ? "Free" : formatPrice(price?.value, price?.currency),
40
- free,
41
- durationMinutes: raw.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? null,
42
- imageUrl: imgSrc(raw.media?.mainMedia?.image, 800, 800),
43
- categoryId: raw.category?._id ?? null,
44
- staff: (raw.staffMemberDetails?.staffMembers ?? [])
45
- .map((m: Raw) => ({ id: m.staffMemberId ?? "", name: m.name ?? "" }))
46
- .filter((m: { id: string }) => m.id),
47
- };
48
- }
49
-
50
- function toDetail(raw: Raw): ServiceDetail {
51
- const summary = toSummary(raw);
52
- const online = raw.payment?.options?.online === true;
53
- const inPerson = raw.payment?.options?.inPerson === true;
54
- // Derive — never hardcode "ONLINE": a free/pay-in-person service booked ONLINE gets
55
- // rejected by the cart with INSUFFICIENT_INVENTORY.
56
- const paymentOption: "ONLINE" | "OFFLINE" = !online && inPerson ? "OFFLINE" : "ONLINE";
57
- return {
58
- ...summary,
59
- description: raw.description ?? "",
60
- formId: raw.form?._id ?? null,
61
- paymentOption,
62
- cancellationFeeEnabled: raw.bookingPolicy?.cancellationFeePolicy?.enabled === true,
63
- };
64
- }
65
-
66
27
  /** List bookable services (hidden ones filtered), mapped to grid-ready DTOs. */
67
28
  export async function fetchServices({ limit = 100 } = {}): Promise<ServiceSummary[]> {
68
29
  const res = await services
69
- .queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] as any })
30
+ .queryServices({ conditionalFields: SERVICES_CONDITIONAL_FIELDS as any })
70
31
  .eq("appId", BOOKINGS_APP_ID)
71
32
  .limit(limit)
72
33
  .find();
73
- return (res.items ?? []).filter((s: Raw) => !s.hidden).map((s: Raw) => toSummary(s));
34
+ return ((res.items ?? []) as Raw[]).filter(isVisible).map((s) => toSummary(s, imgSrc));
74
35
  }
75
36
 
76
37
  /** Fetch one service by its URL slug (mainSlug.name). Null when not found. */
77
38
  export async function fetchServiceBySlug(slug: string): Promise<ServiceDetail | null> {
78
39
  const res = await services
79
- .queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] as any })
40
+ .queryServices({ conditionalFields: SERVICES_CONDITIONAL_FIELDS as any })
80
41
  .eq("mainSlug.name", slug)
81
42
  .eq("appId", BOOKINGS_APP_ID)
82
43
  .limit(1)
83
44
  .find();
84
45
  const raw = res.items?.[0];
85
- return raw ? toDetail(raw as Raw) : null;
46
+ return raw ? toDetail(raw as Raw, imgSrc) : null;
86
47
  }
87
48
 
88
49
  /** Service categories for a filter bar — non-fatal (empty array on failure). */
89
50
  export async function fetchBookingCategories(): Promise<BookingCategory[]> {
90
51
  try {
91
52
  const res = await categories.queryCategories().find();
92
- return (res.items ?? []).map((c: Raw) => ({ id: c._id ?? "", name: c.name ?? "" })).filter((c) => c.id);
53
+ return ((res.items ?? []) as Raw[]).map(toCategory).filter((c) => c.id);
93
54
  } catch {
94
55
  return [];
95
56
  }
@@ -56,12 +56,14 @@ export interface Slot {
56
56
  staff: { id: string; name: string }[];
57
57
  }
58
58
 
59
- /** A booking-form field (flat, from getFormSummary; values are keyed by `target`). */
59
+ /** A booking-form field (flat, from the form summary + schema; values are keyed by `target`). */
60
60
  export interface BookingFormField {
61
61
  target: string;
62
62
  label: string;
63
63
  type: "STRING" | "EMAIL" | "PHONE" | "NUMBER" | "URL";
64
64
  options?: string[];
65
+ /** The form marks it required — the only fields that gate `canBook`; the rest may stay empty. */
66
+ required: boolean;
65
67
  }
66
68
 
67
69
  /** The outcome of book(): either the browser is being redirected, or it's done. */
@@ -0,0 +1,113 @@
1
+ // Availability, the booking form, and the createBooking → Cart V2 → checkout-or-place sequence over
2
+ // REST — the twin of app/wix/bookings/booking.ts. Same exports, same DTOs; every body comes from
3
+ // booking-core (the SAME file the SDK transport uses, deployed flat next to this one), so this file
4
+ // is only the transport: literal paths, one fetch per step. Failures are loud: a taken slot, a
5
+ // refused cart, a checkout that won't start all throw — surface the message to the visitor.
6
+ // All calls run with the visitor token: the booking and its cart are the token's (see ./client).
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slots.md
8
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-event-time-slots.md
9
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/create-booking.md
10
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
11
+ // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
12
+ // docs: https://dev.wix.com/docs/api-reference/crm/forms/form-schemas/get-form-summary.md
13
+ import { wixRequest } from "./client.js";
14
+ import {
15
+ FALLBACK_FIELDS,
16
+ appointmentSlotsRequest,
17
+ bookingCartRequest,
18
+ bookingIdOf,
19
+ bookingRequest,
20
+ cartIdOf,
21
+ cartTotal,
22
+ checkoutRedirectRequest,
23
+ checkoutRequired,
24
+ classSlotsRequest,
25
+ confirmedResult,
26
+ defaultTimeZone,
27
+ toFormFields,
28
+ toLocalDateString,
29
+ toSlot,
30
+ type Raw,
31
+ type SlotsWindow,
32
+ } from "./booking-core.js";
33
+ import type { BookingFormField, BookingResult, ServiceDetail, Slot } from "./types.js";
34
+
35
+ export { toLocalDateString };
36
+
37
+ /**
38
+ * Bookable slots for a service in [from, from + days) — APPOINTMENT and CLASS use different APIs;
39
+ * this branches for you. Dates are local wall-clock; timeZone defaults to the visitor's.
40
+ * APPOINTMENT: POST /service-availability/v2/time-slots { serviceId, fromLocalDate, toLocalDate, timeZone, bookable, cursorPaging, includeResourceTypeIds, resourceTypes? }
41
+ * CLASS: POST /service-availability/v2/time-slots/event { serviceIds, fromLocalDate, toLocalDate, timeZone, includeNonBookable, eventFilter? }
42
+ */
43
+ export async function fetchSlots(service: Pick<ServiceDetail, "id" | "type">, window: SlotsWindow = {}): Promise<Slot[]> {
44
+ const res =
45
+ service.type === "CLASS"
46
+ ? await wixRequest<Raw>("/service-availability/v2/time-slots/event", { body: classSlotsRequest(service.id, window) })
47
+ : await wixRequest<Raw>("/service-availability/v2/time-slots", { body: appointmentSlotsRequest(service.id, window) });
48
+ return ((res?.timeSlots ?? []) as Raw[]).map(toSlot);
49
+ }
50
+
51
+ /**
52
+ * The service's booking-form fields, flat and render-ready (values keyed by `target`). ALWAYS a
53
+ * non-empty list — contact basics when the schema is missing/unusable — so the form renders
54
+ * unconditionally. GET /form-schema-service/v4/forms/{formId}/summary (labels, types) and
55
+ * GET /form-schema-service/v4/forms/{formId} (which fields are required — the summary doesn't say).
56
+ */
57
+ export async function fetchBookingForm(formId: string | null): Promise<BookingFormField[]> {
58
+ if (!formId) return FALLBACK_FIELDS;
59
+ try {
60
+ const id = encodeURIComponent(formId);
61
+ const [res, form] = await Promise.all([
62
+ wixRequest<Raw>(`/form-schema-service/v4/forms/${id}/summary`, { method: "GET" }),
63
+ wixRequest<Raw>(`/form-schema-service/v4/forms/${id}`, { method: "GET" }).catch(() => null),
64
+ ]);
65
+ return toFormFields(res?.formSummary, form?.form);
66
+ } catch {
67
+ return FALLBACK_FIELDS;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Book a slot: createBooking → createCart (holds the seat) → calculateCart → hosted checkout (paid)
73
+ * or placeOrder (free / pay-in-person). Call from the browser. `formValues` is the object your
74
+ * inputs wrote, keyed by field `target` — passed as the formSubmission DIRECTLY. Throws with the
75
+ * refusal (slot taken, invalid form) — surface it, don't swallow it. `origin` is the site's real
76
+ * https origin as registered on the OAuth app's allowed domains (browser: window.location.origin).
77
+ * POST /bookings/v2/bookings { booking, formSubmission }
78
+ * POST /ecom/v2/carts { catalogItems: [{ quantity: 1, catalogReference: { catalogItemId: <bookingId>, appId } }], cart: { source: { channelType: "WEB" } } }
79
+ * POST /ecom/v2/carts/{cartId}/calculate {}
80
+ * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId: <cartId> }, callbacks: { postFlowUrl } }
81
+ * POST /ecom/v2/carts/{cartId}/place-order {}
82
+ */
83
+ export async function bookService(
84
+ service: ServiceDetail,
85
+ slot: Slot,
86
+ formValues: Record<string, unknown>,
87
+ {
88
+ staffId,
89
+ timeZone = defaultTimeZone(),
90
+ origin = typeof window !== "undefined" ? window.location.origin : "",
91
+ }: { staffId?: string; timeZone?: string; origin?: string } = {},
92
+ ): Promise<BookingResult> {
93
+ const created = await wixRequest<Raw>("/bookings/v2/bookings", {
94
+ body: { booking: bookingRequest(service, slot, { staffId, timeZone, idKey: "id" }), formSubmission: formValues },
95
+ });
96
+ const bookingId = bookingIdOf(created);
97
+ if (!bookingId) throw new Error("The booking couldn't be created — the slot may have just been taken.");
98
+
99
+ const newCart = await wixRequest<Raw>("/ecom/v2/carts", { body: bookingCartRequest(bookingId) });
100
+ const cartId = cartIdOf(newCart);
101
+ if (!cartId) throw new Error("The booking couldn't be reserved — please try again.");
102
+
103
+ const calc = await wixRequest<Raw>(`/ecom/v2/carts/${cartId}/calculate`, { body: {} });
104
+ if (checkoutRequired(service, cartTotal(calc))) {
105
+ const session = await wixRequest<Raw>("/headless/v1/redirect-session", { body: checkoutRedirectRequest(cartId, origin) });
106
+ const url = session?.redirectSession?.fullUrl;
107
+ if (!url) throw new Error("Checkout couldn't start — please try again.");
108
+ return { kind: "redirect", url };
109
+ }
110
+
111
+ const order = await wixRequest<Raw>(`/ecom/v2/carts/${cartId}/place-order`, { body: {} });
112
+ return confirmedResult(bookingId, order);
113
+ }
@@ -0,0 +1,55 @@
1
+ // Service reads over REST — the twin of app/wix/bookings/services.ts. Same exports, same DTOs; the
2
+ // rules and mappers come from services-core (the SAME file the SDK transport uses, deployed flat
3
+ // next to this one by deploy.mjs --stack static), so this file is only the transport: one fetch with
4
+ // a literal body per function. Every call here is safe from a browser with a visitor token.
5
+ // Porting: keep the paths, keep the bodies, port the core once.
6
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/query-services.md
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/categories-v2/query-categories.md
8
+ import { wixRequest } from "./client.js";
9
+ import { imgSrc } from "./media.js";
10
+ import {
11
+ BOOKINGS_APP_ID,
12
+ SERVICES_CONDITIONAL_FIELDS,
13
+ STAFF_RESOURCE_TYPE_ID,
14
+ isVisible,
15
+ servicesFilter,
16
+ toCategory,
17
+ toDetail,
18
+ toSummary,
19
+ type Raw,
20
+ } from "./services-core.js";
21
+ import type { BookingCategory, ServiceDetail, ServiceSummary } from "./types.js";
22
+
23
+ export { BOOKINGS_APP_ID, STAFF_RESOURCE_TYPE_ID };
24
+
25
+ // POST /bookings/v2/services/query { query: { filter, paging: { limit } }, conditionalFields }
26
+ async function queryServices(slug: string | undefined, limit: number): Promise<Raw[]> {
27
+ const res = await wixRequest<Raw>("/bookings/v2/services/query", {
28
+ body: { query: { filter: servicesFilter(slug), paging: { limit } }, conditionalFields: SERVICES_CONDITIONAL_FIELDS },
29
+ });
30
+ return (res?.services ?? []) as Raw[];
31
+ }
32
+
33
+ /** List bookable services (hidden ones filtered), mapped to grid-ready DTOs. */
34
+ export async function fetchServices({ limit = 100 } = {}): Promise<ServiceSummary[]> {
35
+ return (await queryServices(undefined, limit)).filter(isVisible).map((s) => toSummary(s, imgSrc));
36
+ }
37
+
38
+ /** One service by its URL slug (mainSlug.name) — a query with the slug in the filter. Null when not found. */
39
+ export async function fetchServiceBySlug(slug: string): Promise<ServiceDetail | null> {
40
+ const raw = (await queryServices(slug, 1))[0];
41
+ return raw ? toDetail(raw, imgSrc) : null;
42
+ }
43
+
44
+ /**
45
+ * Service categories for a filter bar — non-fatal (empty array on failure).
46
+ * POST /bookings/v2/categories/query { query: {} }
47
+ */
48
+ export async function fetchBookingCategories(): Promise<BookingCategory[]> {
49
+ try {
50
+ const res = await wixRequest<Raw>("/bookings/v2/categories/query", { body: { query: {} } });
51
+ return ((res?.categories ?? []) as Raw[]).map(toCategory).filter((c) => c.id);
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
@@ -51,6 +51,16 @@ image leaves that service text-only.
51
51
  **Seeding is additive — never delete or overwrite existing content**; ask first if a cleanup
52
52
  seems needed.
53
53
 
54
+ ## Supplied content
55
+
56
+ The general rules are in `references/shared/SUPPLIED-CONTENT.md`. For bookings, each service the
57
+ user lists is one entry. Its name becomes `name` and its length in minutes becomes `duration`. Its
58
+ price becomes `price`; a service marked free, or with no price, gets `"free": true`. A group activity
59
+ with a capacity or fixed times becomes a `CLASS` with `capacity` and one `sessions` entry per listed
60
+ time; everything else is an `APPOINTMENT`. A category or group becomes `category`. Their photo
61
+ becomes `imageUrl`. A service with no duration is a question for the user. Staff members and
62
+ opening hours are not seeded — say so; the owner sets them in the dashboard.
63
+
54
64
  ## Escape hatch — individual functions
55
65
  `setupBookings` composes exported steps — `installBookingsApp`, `queryStaffWithRetry`,
56
66
  `createCategories`, `createServices`, `scheduleClassSessions`, `importImage`,
@@ -0,0 +1,28 @@
1
+ // What the booking business holds: services, staff, categories.
2
+ // node <SKILL_ROOT>/references/bookings/seed/read-site.mjs [--site <siteId>] [--limit <n>]
3
+ import { runReader } from "../../shared/seed/read-site.mjs";
4
+
5
+ const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
6
+ const D = "https://dev.wix.com/docs/api-reference/business-solutions/bookings";
7
+
8
+ await runReader({
9
+ vertical: "bookings",
10
+ appId: BOOKINGS_APP_ID,
11
+ async read(api, { limit }) {
12
+ const services = await api.call({ path: "/_api/bookings/v2/services/query", body: { query: { paging: { limit }, filter: { hidden: { $eq: false } } } }, docs: `${D}/services/services-v2/query-services` });
13
+ const staff = await api.tryCall({ path: "/bookings/v1/staff-members/query", body: { query: {}, fields: ["RESOURCE_DETAILS"] }, docs: `${D}/staff-members/staff-members/query-staff-members` });
14
+ const cats = await api.tryCall({ path: "/bookings/v2/categories/query", body: { query: {} }, docs: `${D}/services/categories-v2/query-categories` });
15
+ return {
16
+ serviceCount: services.pagingMetadata?.total ?? (services.services ?? []).length,
17
+ services: (services.services ?? []).map((s) => ({
18
+ name: s.name, slug: s.mainSlug?.name ?? null, type: s.type ?? null,
19
+ category: s.category?.name ?? null,
20
+ price: s.payment?.fixed?.price ? `${s.payment.fixed.price.value} ${s.payment.fixed.price.currency}` : s.payment?.varied?.defaultPrice ? `from ${s.payment.varied.defaultPrice.value} ${s.payment.varied.defaultPrice.currency}` : (s.payment?.rateType ?? null),
21
+ durationMinutes: s.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? null,
22
+ online: s.payment?.options?.online ?? false, inPerson: s.payment?.options?.inPerson ?? false, media: s.media?.mainMedia ? 1 : 0,
23
+ })),
24
+ staff: (staff?.staffMembers ?? []).map((m) => m.name),
25
+ categories: (cats?.categories ?? []).map((c) => ({ id: c.id, name: c.name })),
26
+ };
27
+ },
28
+ });