@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.
- package/package.json +1 -1
- package/skills/wix-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
|
@@ -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`,
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
# CMS — playbook
|
|
2
2
|
|
|
3
|
-
The data machinery ships as files — typed reads/writes over ANY collection,
|
|
4
|
-
normalization layer (dates → ISO strings,
|
|
5
|
-
CMS is schema-generic, so unlike other
|
|
6
|
-
(`plan.json`) is the site's content model,
|
|
7
|
-
|
|
8
|
-
write data-access logic; you never skip designing.
|
|
3
|
+
The data machinery ships as files — typed reads/writes over ANY collection, the list and item
|
|
4
|
+
state machines, their React bindings, and the normalization layer (dates → ISO strings,
|
|
5
|
+
`wix:image://` → https URLs), correct end-to-end. CMS is schema-generic, so unlike other
|
|
6
|
+
verticals there are **no fixed pages**: the seed plan (`plan.json`) is the site's content model,
|
|
7
|
+
and **you design the pages from it** — a listing surface and an item page per content collection,
|
|
8
|
+
plus the home page and the brand. You never write data-access logic; you never skip designing.
|
|
9
9
|
|
|
10
10
|
## The file map (deployed into `src/`)
|
|
11
11
|
|
|
12
|
-
**
|
|
13
|
-
|
|
12
|
+
**On Astro and React the shipped files are tested and work as they are** — this table and the
|
|
13
|
+
contracts below are everything you need to use them, so don't spend the run reading their source;
|
|
14
|
+
wire them and build your surfaces. Reading them is the right move when something is off (a runtime
|
|
15
|
+
error, a field this playbook doesn't cover) or when the brief wants a behaviour they don't offer —
|
|
16
|
+
then read the file that owns it and change or extend it. On `lib`, `static`, and a port the hooks
|
|
17
|
+
don't deploy at all; the stores behind them do, and each wiring section below says how to bind
|
|
18
|
+
them. Files you edit: `SiteLayout.astro` and `styles/global.css`. Files you **create** (skeletons
|
|
19
|
+
below): the listing and item pages with their islands, plus your home page.
|
|
14
20
|
|
|
15
21
|
| file | what it is |
|
|
16
22
|
|---|---|
|
|
17
|
-
| `wix/config.ts` · `wix/sdk.ts`
|
|
23
|
+
| `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
|
|
24
|
+
| `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes)` — every `<img>` attribute for a DTO image (`src`, `srcSet`, `sizes`, lazy): `<img {...imgAttrs(item.photo, "33vw")} alt={item.title} />`; `{}` for an absent image — render your placeholder then |
|
|
18
25
|
| `wix/cms/types.ts` | the DTOs (`CmsItem`, `CmsFilter`, `CmsSort`, `CmsQuery`, `CmsPage`) — contracts below |
|
|
19
|
-
| `wix/cms/items.ts` | `queryItems`, `getItemById`, `getItemBy`, `countItems`, `insertItem`, `updateItem`, `patchItemFields`, `removeItem` |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
26
|
+
| `wix/cms/items.ts` | `queryItems`, `getItemById`, `getItemBy`, `countItems`, `insertItem`, `updateItem`, `patchItemFields`, `removeItem` — the transport; the rules and DTO mappers are in `items-core.ts` beside it (shared with the REST layer) |
|
|
27
|
+
| `wix/cms/items-core.ts` | the normalization rules, the filter guard, the wire spelling of a query, and `formatDate(iso)` — a DATE field as copy ("" for absent/invalid, never "Invalid Date") |
|
|
28
|
+
| `wix/cms/collection-store.ts` · `item-store.ts` | the list and item state machines, framework-free (`createCollectionStore()`, `createItemStore()` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
|
|
29
|
+
| `hooks/cms/useCollection.ts` | React binding of `collection-store.ts`: list state + skip paging — contract below |
|
|
30
|
+
| `hooks/cms/useItem.ts` | React binding of `item-store.ts`: one item by `_id` or slug field — contract below |
|
|
22
31
|
| `styles/global.css` | the design system: Tailwind v4 + the `@theme` token block (shared across verticals) |
|
|
23
32
|
|
|
24
|
-
There are **no shipped components** — the data layer and hooks are the machinery; every
|
|
33
|
+
There are **no shipped components** — the data layer, stores, and hooks are the machinery; every
|
|
25
34
|
component is yours.
|
|
26
35
|
|
|
27
36
|
Astro stack additionally gets:
|
|
@@ -48,14 +57,33 @@ pages bind to (collection ids and field keys verbatim).
|
|
|
48
57
|
Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
|
|
49
58
|
Style everything with Tailwind utilities on the tokens.
|
|
50
59
|
|
|
60
|
+
### What a complete content site shows (recommended defaults)
|
|
61
|
+
|
|
62
|
+
Defaults for a brief that says nothing about them; the prompt wins where it differs. Look at the
|
|
63
|
+
seeded content before designing (how many collections, which fields, whether items carry images
|
|
64
|
+
and dates) and design for this content, not for a stereotype of the business.
|
|
65
|
+
|
|
66
|
+
- **Home:** what the site is about and one path into the content in the first screen; real items
|
|
67
|
+
under truthful headings; not a repeat of a listing page.
|
|
68
|
+
- **Listing:** a real card — image, title, a secondary field — in the first screen; each item a
|
|
69
|
+
link to its item page (`/<collection>/<slug>`); loading, empty, and error states that look
|
|
70
|
+
different; "load more" when `hasNext`.
|
|
71
|
+
- **Item page:** title, image, the date formatted, the body as HTML, related items only when a
|
|
72
|
+
reference field carries them; a slug that resolves to nothing shows only the not-found state.
|
|
73
|
+
- **Copy:** nothing the owner didn't supply — no invented fields, counts, or authors; no Wix IDs or
|
|
74
|
+
technical words in visible text.
|
|
75
|
+
|
|
51
76
|
### The contracts your components consume
|
|
52
77
|
|
|
78
|
+
Everything you need — tested and working as they are; read a file when something is off or the
|
|
79
|
+
brief wants more.
|
|
80
|
+
|
|
53
81
|
```ts
|
|
54
82
|
// CmsItem — display-ready, fields FLAT on the item (never item.data.*):
|
|
55
83
|
// { _id, _createdDate?, _updatedDate?, _owner?, ...fields }
|
|
56
84
|
// TEXT/URL/EMAIL → string · NUMBER → number · BOOLEAN → boolean
|
|
57
|
-
// DATE/DATETIME → ISO string (render:
|
|
58
|
-
// IMAGE → resolved https URL (
|
|
85
|
+
// DATE/DATETIME → ISO string (render: formatDate(item.publishDate) from wix/cms/items-core)
|
|
86
|
+
// IMAGE → resolved https URL (<img {...imgAttrs(item.photo, "33vw")} />; {} when absent → your placeholder)
|
|
59
87
|
// RICH_TEXT → the stored HTML string (render via set:html on a wrapper you control)
|
|
60
88
|
// REFERENCE/MULTI_REFERENCE → id(s); full CmsItem(s) when queried with include
|
|
61
89
|
|
|
@@ -81,40 +109,185 @@ Style everything with Tailwind utilities on the tokens.
|
|
|
81
109
|
// removeItem(collectionId, id)
|
|
82
110
|
```
|
|
83
111
|
|
|
112
|
+
### The pages and islands you create — skeletons
|
|
113
|
+
|
|
114
|
+
The class names in these skeletons are the Astro/React spelling of layout rules that hold on
|
|
115
|
+
every stack, and each rule is also named in words beside its classes. On a stack where the hooks
|
|
116
|
+
don't deploy (`lib`, `static`, a port), keep the rule and write it in your own CSS.
|
|
117
|
+
|
|
118
|
+
Each page is a thin SSR shell (fetch → DTO props → island, or no island when the page has no
|
|
119
|
+
interactivity); each island is a thin view over a hook. Hooks first, branches after. The islands
|
|
120
|
+
render on the server too (`client:load` SSRs), and by then the 200 and headers are already sent — a
|
|
121
|
+
render throw truncates the body mid-stream. Render every state totally; nothing in a render path may
|
|
122
|
+
throw.
|
|
123
|
+
|
|
124
|
+
```astro
|
|
125
|
+
---
|
|
126
|
+
// src/pages/recipes.astro — YOU create it (one per content collection). Items are fetched
|
|
127
|
+
// SERVER-SIDE (view-source shows titles) and handed to the island as serialized DTO props.
|
|
128
|
+
import SiteLayout from "../layouts/SiteLayout.astro";
|
|
129
|
+
import CollectionView from "../components/cms/CollectionView";
|
|
130
|
+
import { queryItems } from "../wix/cms/items";
|
|
131
|
+
import type { CmsPage } from "../wix/cms/types";
|
|
132
|
+
const SORT = [{ field: "publishDate", direction: "desc" as const }];
|
|
133
|
+
let page: CmsPage = { items: [], hasNext: false, total: null };
|
|
134
|
+
try {
|
|
135
|
+
page = await queryItems("recipes", { sort: SORT, limit: 12 });
|
|
136
|
+
} catch {
|
|
137
|
+
// Guarded: an unguarded SSR throw truncates the response mid-stream; the island renders your empty state.
|
|
138
|
+
}
|
|
139
|
+
---
|
|
140
|
+
<SiteLayout title="Recipes">
|
|
141
|
+
<!-- your page heading / intro, then: -->
|
|
142
|
+
<CollectionView client:load collectionId="recipes" sort={SORT} limit={12} initialItems={page.items} initialHasNext={page.hasNext} />
|
|
143
|
+
</SiteLayout>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```astro
|
|
147
|
+
---
|
|
148
|
+
// src/pages/recipes/[slug].astro — YOU create it. SEO is plain <title>/<meta name="description">
|
|
149
|
+
// from the DTO via the layout props — CMS collections have NO owner-editable SEO item type, so
|
|
150
|
+
// don't copy another vertical's wixMetadata/<SEO.Tags> wiring.
|
|
151
|
+
import SiteLayout from "../../layouts/SiteLayout.astro";
|
|
152
|
+
import { getItemBy } from "../../wix/cms/items";
|
|
153
|
+
import { formatDate } from "../../wix/cms/items-core";
|
|
154
|
+
import { imgAttrs } from "../../wix/media";
|
|
155
|
+
let item = null;
|
|
156
|
+
try {
|
|
157
|
+
item = await getItemBy("recipes", "slug", Astro.params.slug!, { include: ["categories"] });
|
|
158
|
+
} catch {
|
|
159
|
+
// Guarded: an unhandled SSR throw truncates the response mid-stream.
|
|
160
|
+
}
|
|
161
|
+
// A missing slug is a real 404 — never a fallback to another item.
|
|
162
|
+
if (!item) return new Response(null, { status: 404 });
|
|
163
|
+
---
|
|
164
|
+
<SiteLayout title={String(item.title)} description={String(item.summary ?? "")}>
|
|
165
|
+
<!-- your detail layout: title; the image through imgAttrs (a placeholder when it returns {});
|
|
166
|
+
formatDate(item.publishDate); the RICH_TEXT body with set:html on a wrapper you control;
|
|
167
|
+
included reference items by name — never an id as content -->
|
|
168
|
+
</SiteLayout>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// src/components/cms/CollectionView.tsx — YOU build it; a listing page mounts it.
|
|
173
|
+
import { useCollection } from "../../hooks/cms/useCollection";
|
|
174
|
+
import { formatDate } from "../../wix/cms/items-core";
|
|
175
|
+
import { imgAttrs } from "../../wix/media";
|
|
176
|
+
import type { CmsFilter, CmsItem, CmsSort } from "../../wix/cms/types";
|
|
177
|
+
|
|
178
|
+
export default function CollectionView(props: {
|
|
179
|
+
collectionId: string;
|
|
180
|
+
filters?: CmsFilter[];
|
|
181
|
+
sort?: CmsSort[];
|
|
182
|
+
limit?: number;
|
|
183
|
+
initialItems?: CmsItem[]; // SSR props from your page — pass straight to useCollection;
|
|
184
|
+
initialHasNext?: boolean; // omitted in a SPA (client fetch)
|
|
185
|
+
}) {
|
|
186
|
+
const { collectionId, ...options } = props;
|
|
187
|
+
const list = useCollection(collectionId, options);
|
|
188
|
+
// …you implement the render:
|
|
189
|
+
// • error → a short inline message
|
|
190
|
+
// • items === null → skeleton cards; [] → your honest empty state (no mock items)
|
|
191
|
+
// • else YOUR grid of YOUR cards: image via <img {...imgAttrs(item.photo, "(min-width: 768px) 33vw, 100vw")} alt={String(item.title)} />
|
|
192
|
+
// — src, srcSet, sizes and lazy loading in one spread; {} when the field is absent → your
|
|
193
|
+
// placeholder; title WRAPS (`min-w-0`, `break-words`), no `truncate`; the date as
|
|
194
|
+
// formatDate(item.publishDate); the card links to `/${collectionId}/${item.slug}`
|
|
195
|
+
// • hasNext → your "load more" control calling loadMore() (disabled while loadingMore)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### The reference files for stacks where the components don't deploy
|
|
200
|
+
|
|
201
|
+
On `lib`, `static`, and a port, nothing under `hooks/` arrives, and CMS ships no components. The
|
|
202
|
+
state machines behind the hooks do arrive — `wix/cms/collection-store.ts`, `item-store.ts` — so you
|
|
203
|
+
never rewrite them: create a store per surface, `subscribe`, render from `getState()`, call its
|
|
204
|
+
actions. Their `CollectionState` / `ItemState` interfaces are the render contract; read those. What
|
|
205
|
+
you write is the rendering — cards, the item view, the states — to the skeletons above. The two
|
|
206
|
+
hooks under `references/cms/app/hooks/cms/` are the binding pattern, ten lines each, if your
|
|
207
|
+
framework's external-store primitive is unfamiliar.
|
|
208
|
+
|
|
84
209
|
### Wiring — Astro (default)
|
|
85
210
|
|
|
86
|
-
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass) and add one nav
|
|
87
|
-
|
|
88
|
-
2. Author your pages under `src/pages/` — SSR fetch in the frontmatter,
|
|
89
|
-
islands (`client:load`; a page with no interactivity needs no island at all).
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
211
|
+
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass) and add one nav link
|
|
212
|
+
per content surface.
|
|
213
|
+
2. Author your pages under `src/pages/` per the skeletons above — SSR fetch in the frontmatter,
|
|
214
|
+
DTO props to your islands (`client:load`; a page with no interactivity needs no island at all).
|
|
215
|
+
3. Write `pages/index.astro` (home) on `SiteLayout`.
|
|
216
|
+
4. **Author your surfaces in as few messages as possible** — batch multiple Writes per message
|
|
217
|
+
(components and pages are independent files).
|
|
218
|
+
|
|
219
|
+
### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
|
|
220
|
+
|
|
221
|
+
`deploy.mjs cms --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts` (the
|
|
222
|
+
visitor client, configured with the public client id), `media.ts`, `money.ts`, and `wix/cms/` —
|
|
223
|
+
`items.ts`, `types.ts`, `items-core.ts`, and the two stores `collection-store.ts`, `item-store.ts`.
|
|
224
|
+
None of it is React. The hooks don't ship on this stack; the stores replace them, and you write the
|
|
225
|
+
components in your framework to the contracts on this page:
|
|
226
|
+
|
|
227
|
+
- bind the stores with your framework's external-store primitive (Vue: `shallowRef` updated in
|
|
228
|
+
`subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
|
|
229
|
+
Solid: a signal set in `subscribe`). `createCollectionStore({ collectionId, filters?, sort?, limit?, include?, initialItems? })`
|
|
230
|
+
per listing (`start()` when mounted, `stop()` when unmounted, `setQuery()` when filters change,
|
|
231
|
+
`loadMore()`), `createItemStore({ collectionId, ref: { id } | { by }, initialItem? })` per item
|
|
232
|
+
surface. State in, actions out — exactly the hooks' contracts above.
|
|
233
|
+
|
|
234
|
+
Routes `/<collection>`, `/<collection>/:slug` (via `getItemBy`, null → your 404); dev server on
|
|
235
|
+
4321; a static build goes through `npx @wix/cli@latest release` with `site.outputDirectory`
|
|
236
|
+
pointing at the build folder, an SSR build is hosted by you. Page tags from the item's own fields.
|
|
237
|
+
|
|
238
|
+
### Wiring — static site (`--stack static`, no bundler)
|
|
239
|
+
|
|
240
|
+
`deploy.mjs cms --stack static --out site` put the REST layer in `site/js/wix/` (browser ESM, the
|
|
241
|
+
`.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` — pages,
|
|
242
|
+
styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project root
|
|
243
|
+
(config, plan, seed output) is never the upload. Same function names and DTOs as the table above,
|
|
244
|
+
so the contracts on this page hold unchanged: `queryItems`, `getItemBy`, `getItemById`,
|
|
245
|
+
`countItems` (and the writes) from `./js/wix/items.js`; `formatDate` from `./js/wix/items-core.js`;
|
|
246
|
+
`imgAttrs` from `./js/wix/media.js`. The state machines ship too: `createCollectionStore` from
|
|
247
|
+
`./js/wix/collection-store.js` (the listing — `start()` once the page is up, `loadMore()`,
|
|
248
|
+
`setQuery()`), `createItemStore` from `./js/wix/item-store.js` (the item page — `notFound` is your
|
|
249
|
+
404 state). No components ship — you write the rendering in plain JS: one render function per
|
|
250
|
+
surface that reads `getState()`, called from `subscribe`, with the surface's controls calling the
|
|
251
|
+
store's actions. Pages are `<collection>.html` and `item.html?collection=…&slug=…` (Wix static
|
|
252
|
+
hosting serves files, not directories — name the file and link to it). Set `document.title` and
|
|
253
|
+
the meta description from the item's fields once it loads. The visitor token persists in
|
|
254
|
+
`localStorage` on its own; never mint per page. Reads work on any collection whose `read` is
|
|
255
|
+
`ANYONE`; a write from the page succeeds only where the seed opened that verb (403 → a permissions
|
|
256
|
+
step). `npx @wix/cli@latest release` uploads `site/`.
|
|
257
|
+
|
|
258
|
+
### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
|
|
259
|
+
|
|
260
|
+
Run `deploy.mjs cms --stack static` in the project folder anyway: `js/wix/` is both the
|
|
261
|
+
browser-side code and the readable spec. Then split by where the call runs. **Reads on the
|
|
262
|
+
server:** port `js/wix/items.ts` and `items-core.ts` to your language — `queryItems`, `getItemBy`,
|
|
263
|
+
`getItemById`, `countItems` returning the same DTO shapes as dicts (the core's `toValue` rules:
|
|
264
|
+
`{ "$date": iso }` → ISO string, `wix:image://` → the https URL form in `media.ts`), one anonymous
|
|
265
|
+
visitor token per process for these public reads (mint and refresh per `client.ts`) — and render
|
|
266
|
+
the listing and item pages in your templates to the contracts above, so titles are in the HTML;
|
|
267
|
+
page tags from the item's fields. **Visitor writes in the browser** (a collection the seed opened
|
|
268
|
+
to visitors or members): `insertItem` from `./js/wix/items.js` on the page, exactly as the static
|
|
269
|
+
wiring above — the browser owns the visitor token, so the server handles no per-visitor tokens.
|
|
270
|
+
Routes stay `/<collection>`, `/<collection>/<slug>`.
|
|
271
|
+
|
|
272
|
+
**Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for the
|
|
273
|
+
reads, run at build time with one anonymous token; the generator must emit every item page (walk
|
|
274
|
+
`queryItems` by `skip` until `hasNext` is false — never only the first page). Run
|
|
275
|
+
`deploy.mjs cms --stack static --out <build dir>` so `js/wix/` is inside the output the pages
|
|
276
|
+
import from, point `site.outputDirectory` at that folder, `wix release`. Pages sit at different
|
|
277
|
+
depths (`/`, `/<collection>/…`): give the templates one base path to `js/wix/` (a template
|
|
278
|
+
variable, or root-relative `/js/wix/…`), never a relative `./js/wix/` — it breaks one level down.
|
|
279
|
+
The frozen listing is the first paint; a filter or "load more" still runs client-side on it
|
|
280
|
+
through `createCollectionStore()` from `./js/wix/collection-store.js`, exactly as on a static
|
|
281
|
+
site. Close with the live URL, the rebuild + release command, and one line for the owner:
|
|
282
|
+
dashboard edits to the content reach the site when that command runs.
|
|
112
283
|
|
|
113
284
|
### Wiring — React SPA (Vite etc.)
|
|
114
285
|
|
|
115
286
|
Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
|
|
116
287
|
plugins — deploy added the dep). Routes are yours: a list route per collection on
|
|
117
|
-
`useCollection`, a detail route on `useItem` with `by: { field: "slug", value }
|
|
288
|
+
`useCollection`, a detail route on `useItem` with `by: { field: "slug", value }` (components
|
|
289
|
+
fetch client-side when no `initialItems` / `initialItem` is passed). Deploy wrote the public
|
|
290
|
+
client id into `wix/config.ts`; nothing else to configure.
|
|
118
291
|
|
|
119
292
|
## Hard rules
|
|
120
293
|
|
|
@@ -132,7 +305,8 @@ plugins — deploy added the dep). Routes are yours: a list route per collection
|
|
|
132
305
|
or use `patchItemFields`. Date fields on any write are Date objects — a round-tripped ISO
|
|
133
306
|
string is silently stored as text and breaks date queries.
|
|
134
307
|
- **Writes 403 unless the collection was seeded with that verb open** — surface a 403 as a
|
|
135
|
-
permissions setup step (which permission, where in the dashboard), not a code bug.
|
|
308
|
+
permissions setup step (which permission, where in the dashboard), not a code bug. Anything
|
|
309
|
+
that needs elevated access runs server-side per `references/shared/CUSTOM_OPERATIONS.md`.
|
|
136
310
|
- **User-created content is a CMS contribution flow.** For a member or visitor submission that
|
|
137
311
|
the app must later list, use the seeded CMS collection and its intended permission preset.
|
|
138
312
|
If the operation needs elevated access — especially a browser-created file that must enter
|
|
@@ -145,17 +319,31 @@ plugins — deploy added the dep). Routes are yours: a list route per collection
|
|
|
145
319
|
returned Wix Media reference. Do not author another upload endpoint for that flow.
|
|
146
320
|
- **RICH_TEXT is HTML, not plain text** — render it with `set:html` (Astro) /
|
|
147
321
|
`dangerouslySetInnerHTML` (React) on a wrapper; never interpolate it as text.
|
|
322
|
+
- **Dates through `formatDate`**, images through `imgAttrs` — the DTO's ISO string and https URL
|
|
323
|
+
are data, not copy.
|
|
148
324
|
- Reference fields hold ids unless the query passed `include` — don't render an id as
|
|
149
325
|
content.
|
|
150
|
-
- Guard absent IMAGE fields — render a fallback, never an empty/broken `<img>`.
|
|
151
|
-
-
|
|
326
|
+
- Guard absent IMAGE fields — `imgAttrs` returns `{}`; render a fallback, never an empty/broken `<img>`.
|
|
327
|
+
- **Page at the source, not on a loaded list.** `hasNext`/`loadMore()` fetch the next page from
|
|
328
|
+
Wix; never raise the page limit instead of paging, never filter or sort client-side a page you
|
|
329
|
+
already hold — pass `filters`/`sort` and let Wix apply them across the collection.
|
|
330
|
+
- **Call every hook before any conditional return.** An island that returns early for
|
|
331
|
+
`notFound`/loading above its hooks changes hook order between renders and React throws.
|
|
332
|
+
- Where the shipped code deploys with Tailwind (Astro, React): theme via the `@theme` tokens, your
|
|
333
|
+
markup on the same tokens; no parallel theme files, no hardcoded palettes. Where it doesn't
|
|
334
|
+
(`lib`, `static`, a port): style with whatever your stack does well, on one token set of your
|
|
335
|
+
own; the rule that survives is the token set, not Tailwind.
|
|
152
336
|
- Live data or an honest empty state — never mock items or invent fields not in the plan.
|
|
153
337
|
|
|
154
338
|
## Point the user to their dashboard
|
|
155
339
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
340
|
+
Hand the owner these links — `{siteId}` is `siteId` in `wix.config.json` (the deploy JSON prints it as
|
|
341
|
+
`dashboardUrl`); a collection id is its `_id` (the seed result lists them).
|
|
342
|
+
|
|
343
|
+
| page | `https://manage.wix.com/dashboard/{siteId}/` + |
|
|
344
|
+
|---|---|
|
|
345
|
+
| Collections | `wix-cms` |
|
|
346
|
+
| A collection's items (fields, and More Actions → Permissions & Privacy) | `wix-cms/data/{collectionId}` |
|
|
159
347
|
|
|
160
348
|
## Seeding
|
|
161
349
|
|
|
@@ -163,17 +351,3 @@ Per `seed/SEED.md` — plain-data `plan.json` into `seed-cms.mjs` from the proje
|
|
|
163
351
|
plan is the content model your pages bind to: design collections that exercise the UI
|
|
164
352
|
(a `slug` field per detail page, an IMAGE field with a verified `imageUrl` per item, a DATE
|
|
165
353
|
field when the content is chronological).
|
|
166
|
-
|
|
167
|
-
## Verify (before declaring done)
|
|
168
|
-
|
|
169
|
-
- [ ] Each listing page renders live items SSR (view-source shows titles) through YOUR
|
|
170
|
-
components; an empty collection shows your honest empty state (no mock items).
|
|
171
|
-
- [ ] Item pages resolve by slug with a real 404 on a bad slug.
|
|
172
|
-
- [ ] Images render (resolved https URLs, no raw `wix:image://`); absent images fall back
|
|
173
|
-
gracefully.
|
|
174
|
-
- [ ] Dates render formatted (no "Invalid Date", no raw ISO strings in copy).
|
|
175
|
-
- [ ] RICH_TEXT renders formatted (no visible HTML tags, no `[object Object]`).
|
|
176
|
-
- [ ] Any 403 surfaced as a permissions step with the dashboard link.
|
|
177
|
-
- [ ] Listing/item/home surfaces are YOUR designs on the tokens; data-layer/hook files
|
|
178
|
-
unedited.
|
|
179
|
-
- [ ] Dashboard link handed to the owner.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// React binding of the collection store (wix/cms/collection-store.ts) — the list state machine
|
|
2
|
+
// lives there, framework-free; this hook subscribes to one instance per mounted listing. SSR-friendly:
|
|
3
|
+
// pass server-fetched items as `initialItems` (Astro frontmatter / server component) and no client
|
|
4
|
+
// fetch happens for the first page; a SPA passes nothing. Changing filters/sort (by value) refetches;
|
|
5
|
+
// loadMore() appends the next page (skip paging). Astro islands and React SPAs use this; a static
|
|
6
|
+
// page, Vue, or Svelte uses the store directly.
|
|
7
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
8
|
+
import { createCollectionStore, type CollectionStore } from "../../wix/cms/collection-store";
|
|
7
9
|
import type { CmsFilter, CmsItem, CmsSort } from "../../wix/cms/types";
|
|
8
10
|
|
|
9
11
|
export interface UseCollectionOptions {
|
|
@@ -30,54 +32,19 @@ export interface UseCollection {
|
|
|
30
32
|
|
|
31
33
|
export function useCollection(collectionId: string, options: UseCollectionOptions = {}): UseCollection {
|
|
32
34
|
const { filters, sort, limit = 20, include, initialItems, initialHasNext } = options;
|
|
35
|
+
const ref = useRef<CollectionStore | null>(null);
|
|
36
|
+
if (!ref.current) ref.current = createCollectionStore({ collectionId, filters, sort, limit, include, initialItems, initialHasNext });
|
|
37
|
+
const store = ref.current;
|
|
33
38
|
// Query identity by value — Dates in filters serialize to ISO, so this is stable.
|
|
34
39
|
const key = JSON.stringify([collectionId, filters ?? null, sort ?? null, limit, include ?? null]);
|
|
35
|
-
const initialKey = useRef<string | null>(initialItems ? key : null);
|
|
36
|
-
const [items, setItems] = useState<CmsItem[] | null>(initialItems ?? null);
|
|
37
|
-
const [hasNext, setHasNext] = useState<boolean>(
|
|
38
|
-
initialHasNext ?? (initialItems ? initialItems.length >= limit : false),
|
|
39
|
-
);
|
|
40
|
-
const [loadingMore, setLoadingMore] = useState(false);
|
|
41
|
-
const [error, setError] = useState<string | null>(null);
|
|
42
|
-
const itemsRef = useRef<CmsItem[] | null>(items);
|
|
43
|
-
itemsRef.current = items;
|
|
44
|
-
|
|
45
40
|
useEffect(() => {
|
|
46
|
-
|
|
47
|
-
initialKey.current = null;
|
|
48
|
-
let alive = true;
|
|
49
|
-
setItems(null);
|
|
50
|
-
setError(null);
|
|
51
|
-
queryItems(collectionId, { filters, sort, limit, include })
|
|
52
|
-
.then((page) => {
|
|
53
|
-
if (!alive) return;
|
|
54
|
-
setItems(page.items);
|
|
55
|
-
setHasNext(page.hasNext);
|
|
56
|
-
})
|
|
57
|
-
.catch((e) => {
|
|
58
|
-
if (!alive) return;
|
|
59
|
-
setItems([]);
|
|
60
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
61
|
-
});
|
|
62
|
-
return () => {
|
|
63
|
-
alive = false;
|
|
64
|
-
};
|
|
65
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
-
}, [key]);
|
|
67
|
-
|
|
68
|
-
const loadMore = useCallback(() => {
|
|
69
|
-
const current = itemsRef.current;
|
|
70
|
-
if (!current || loadingMore) return;
|
|
71
|
-
setLoadingMore(true);
|
|
72
|
-
queryItems(collectionId, { filters, sort, limit, include, skip: current.length })
|
|
73
|
-
.then((page) => {
|
|
74
|
-
setItems([...(itemsRef.current ?? []), ...page.items]);
|
|
75
|
-
setHasNext(page.hasNext);
|
|
76
|
-
})
|
|
77
|
-
.catch((e) => setError(e instanceof Error ? e.message : String(e)))
|
|
78
|
-
.finally(() => setLoadingMore(false));
|
|
41
|
+
store.setQuery({ collectionId, filters, sort, limit, include });
|
|
79
42
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
-
}, [
|
|
81
|
-
|
|
82
|
-
|
|
43
|
+
}, [store, key]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
store.start();
|
|
46
|
+
return () => store.stop();
|
|
47
|
+
}, [store]);
|
|
48
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
49
|
+
return { items: state.items, hasNext: state.hasNext, loadMore: store.loadMore, loadingMore: state.loadingMore, error: state.error };
|
|
83
50
|
}
|