@wix/agent-skills 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +2 -1
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +14 -0
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +11 -6
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +3 -17
- package/skills/wix-app/references/editor-react-component/SSR.md +119 -0
- package/skills/wix-design-system/SKILL.md +1 -1
- package/skills/wix-headless/references/BUILD-astro.md +5 -7
- package/skills/wix-headless/references/SETUP.md +1 -1
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +58 -428
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +4 -7
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +158 -143
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +17 -13
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +16 -78
- package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +122 -179
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +61 -9
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +10 -2
- package/skills/wix-headless/references/bookings/FLOW.md +185 -0
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +60 -76
- package/skills/wix-headless/references/custom/bookings/WIRING.md +123 -65
- package/skills/wix-headless/references/verticals/bookings.md +6 -8
- package/skills/wix-manage/SKILL.md +12 -0
- package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
- package/skills/wix-manage/references/bookings/create-appointment-service.md +266 -0
- package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
- package/skills/wix-manage/references/bookings/create-class-service.md +245 -0
- package/skills/wix-manage/references/bookings/create-course-service.md +249 -0
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
- package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
- package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
- package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
- package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
- package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
- package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
|
@@ -5,26 +5,77 @@ import Layout from "../../layouts/Layout.astro";
|
|
|
5
5
|
import SeoTags from "../../components/SeoTags.astro";
|
|
6
6
|
import ServiceBookingFlow from "../../components/ServiceBookingFlow";
|
|
7
7
|
import { services } from "@wix/bookings";
|
|
8
|
+
import { forms } from "@wix/forms";
|
|
8
9
|
import { auth } from "@wix/essentials";
|
|
9
10
|
|
|
11
|
+
const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
|
|
10
12
|
const { slug } = Astro.params;
|
|
11
13
|
|
|
12
14
|
let service: any = null;
|
|
13
15
|
try {
|
|
14
|
-
// Ambient SSR via @wix/essentials
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(
|
|
20
|
-
|
|
21
|
-
)
|
|
16
|
+
// Ambient SSR via @wix/essentials. Single service by slug: the .eq() builder
|
|
17
|
+
// chain on mainSlug.name + appId (the form the @wix/bookings SDK expects).
|
|
18
|
+
const result = await auth
|
|
19
|
+
.elevate(services.queryServices)({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
|
|
20
|
+
.eq("mainSlug.name", slug)
|
|
21
|
+
.eq("appId", BOOKING_APP_ID)
|
|
22
|
+
.limit(1)
|
|
23
|
+
.find();
|
|
24
|
+
service = result.items?.[0] ?? null;
|
|
22
25
|
} catch (err) {
|
|
23
26
|
console.error(`[services/${slug}] detail query failed:`, err);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
if (!service) return Astro.redirect("/services", 302);
|
|
27
30
|
|
|
31
|
+
// Booking-form SCHEMA — the form belongs to the service (service.form._id) in the
|
|
32
|
+
// bookings namespace. Read it server-side and pass the field list to the island.
|
|
33
|
+
// Same renderer the forms vertical uses (references/astro/forms/CONTACT_FORM.md):
|
|
34
|
+
// keep the field's `target` — that is the key createBooking expects in formSubmission.
|
|
35
|
+
let formFields: any[] = [];
|
|
36
|
+
try {
|
|
37
|
+
const formId = service.form?._id;
|
|
38
|
+
if (formId) {
|
|
39
|
+
const res: any = await auth.elevate(forms.getForm)(formId);
|
|
40
|
+
const form = res?.form ?? res;
|
|
41
|
+
// Only render field types we can submit as a simple value. Skip complex,
|
|
42
|
+
// object-valued fields (e.g. a multi-line ADDRESS) — they carry no string
|
|
43
|
+
// `componentType`, and sending a string for them fails createBooking with
|
|
44
|
+
// "must be object". The booking enforces only the contact basics
|
|
45
|
+
// (first_name/last_name/email), so skipping optional complex fields is safe.
|
|
46
|
+
const RENDERABLE = ["TEXT_INPUT", "PHONE_INPUT", "DROPDOWN"];
|
|
47
|
+
formFields = (form?.formFields ?? [])
|
|
48
|
+
.filter(
|
|
49
|
+
(f: any) =>
|
|
50
|
+
f.fieldType === "INPUT" &&
|
|
51
|
+
!f.hidden &&
|
|
52
|
+
RENDERABLE.includes(f.inputOptions?.stringOptions?.componentType),
|
|
53
|
+
)
|
|
54
|
+
.map((f: any) => {
|
|
55
|
+
const opts = f.inputOptions;
|
|
56
|
+
const str = opts?.stringOptions;
|
|
57
|
+
const label =
|
|
58
|
+
str?.textInputOptions?.label ??
|
|
59
|
+
str?.dropdownOptions?.label ??
|
|
60
|
+
str?.phoneInputOptions?.label ??
|
|
61
|
+
"";
|
|
62
|
+
return {
|
|
63
|
+
label,
|
|
64
|
+
target: opts?.target ?? "",
|
|
65
|
+
required: opts?.required ?? false,
|
|
66
|
+
componentType: str?.componentType ?? "TEXT_INPUT",
|
|
67
|
+
identifier: f.identifier ?? "",
|
|
68
|
+
options: str?.dropdownOptions?.options?.map((o: any) => ({
|
|
69
|
+
value: o.value ?? o.label ?? "",
|
|
70
|
+
label: o.label ?? o.value ?? "",
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`[services/${slug}] booking form schema failed:`, err);
|
|
77
|
+
}
|
|
78
|
+
|
|
28
79
|
const formatPrice = (p: any): string | undefined => {
|
|
29
80
|
if (!p || p.rateType === "NO_FEE") return undefined;
|
|
30
81
|
if (p.rateType === "FIXED" && p.fixed?.price) {
|
|
@@ -71,9 +122,10 @@ const seoTags = service.seoData?.tags ?? [];
|
|
|
71
122
|
<section class="border-t border-rule pt-2xl">
|
|
72
123
|
<h2 class="font-display text-3xl leading-tight mb-xl">Book this service</h2>
|
|
73
124
|
<ServiceBookingFlow
|
|
74
|
-
|
|
125
|
+
service={service}
|
|
75
126
|
serviceName={serviceName}
|
|
76
127
|
serviceType={service.type ?? "APPOINTMENT"}
|
|
128
|
+
fields={formFields}
|
|
77
129
|
client:only="react"
|
|
78
130
|
/>
|
|
79
131
|
</section>
|
|
@@ -7,13 +7,21 @@ import ServiceCard from "../../components/ServiceCard.astro";
|
|
|
7
7
|
import { services } from "@wix/bookings";
|
|
8
8
|
import { auth } from "@wix/essentials";
|
|
9
9
|
|
|
10
|
+
// Wix Bookings app id — the base filter must scope queryServices to it.
|
|
11
|
+
const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
|
|
12
|
+
|
|
10
13
|
let serviceList: any[] = [];
|
|
11
14
|
try {
|
|
12
15
|
// Ambient @wix/astro SSR client via @wix/essentials (same pattern as CMS pages).
|
|
13
16
|
// Do NOT build createClient({ auth: OAuthStrategy({ clientId: import.meta.env.* }) }) —
|
|
14
17
|
// that env var is client-only and undefined at server render → 500.
|
|
15
|
-
//
|
|
16
|
-
const result = await auth
|
|
18
|
+
// Pass the request object (filter must include appId) + conditionalFields, then .find().
|
|
19
|
+
const result = await auth
|
|
20
|
+
.elevate(services.queryServices)({
|
|
21
|
+
query: { filter: { appId: BOOKING_APP_ID }, paging: { limit: 100 } },
|
|
22
|
+
conditionalFields: ["STAFF_MEMBER_DETAILS"],
|
|
23
|
+
})
|
|
24
|
+
.find();
|
|
17
25
|
serviceList = (result.items ?? []).filter((s: any) => !s.hidden);
|
|
18
26
|
} catch (err) {
|
|
19
27
|
console.error("[services] listing query failed:", err);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Bookings — the booking flow (framework-agnostic logic)
|
|
2
|
+
|
|
3
|
+
The booking logic — the step model, the shared state, and the exact `@wix` SDK
|
|
4
|
+
sequence — is the **same on every frontend** (astro, react/vite, vue, plain HTML).
|
|
5
|
+
This file is that shared logic. The per-framework wiring layers on top:
|
|
6
|
+
|
|
7
|
+
- **astro** (`frontendBuild: wix`): `../astro/bookings/SERVICES_PAGES.md` (SSR read pages) + `../astro/bookings/COMPONENTS.md` (the client islands).
|
|
8
|
+
- **own / static** (`frontendBuild: own`/`none`): `../custom/bookings/WIRING.md`.
|
|
9
|
+
|
|
10
|
+
**Code examples are React** (`../astro/templates/bookings/*.tsx` + `bookingDriver.ts`).
|
|
11
|
+
React is directly usable on astro islands and on react/vite. **On any other
|
|
12
|
+
framework, translate the examples** — the SDK calls and payloads are identical;
|
|
13
|
+
only the UI idiom (`useState`/`useEffect` → a store / `onMounted` / signals) and
|
|
14
|
+
the client acquisition differ (see "Client identity" below).
|
|
15
|
+
|
|
16
|
+
The flow is **appointment booking**. CLASS sign-up reuses the same sequence with
|
|
17
|
+
an event-slot branch (noted inline) and is a fast-follow, not v1. **Location and
|
|
18
|
+
staff selection are likewise a fast-follow (a separate PR); add-ons are out of
|
|
19
|
+
scope.** v1 threads a single service selection — the shared state below holds no
|
|
20
|
+
location / staff / add-on choice yet.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. The step model
|
|
25
|
+
|
|
26
|
+
A single customer journey, five steps, holding one shared selection:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
1. Catalog list bookable services → pick a service
|
|
30
|
+
2. Slots week calendar of availability for that service → pick a time slot
|
|
31
|
+
3. Details the service's booking form (collect contact) → submit
|
|
32
|
+
4. Book run the SDK sequence (§3)
|
|
33
|
+
5a. paid → redirect to the Wix-hosted checkout → returns to the confirmation page
|
|
34
|
+
5b. free/offline → place the order → confirmation page
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**The catalog is the entry point but optional** — a visitor can land directly on a
|
|
38
|
+
service page and start at the Slots step. Every step rehydrates from the URL (§2),
|
|
39
|
+
so deep links work.
|
|
40
|
+
|
|
41
|
+
Mapping the steps onto pages/routes is the **target framework's choice** — translate
|
|
42
|
+
the step model to whatever router the framework uses. The skill recommends the routes
|
|
43
|
+
`/services`, `/services/[slug]`, `/booking-confirmation` (advertised in
|
|
44
|
+
`../verticals/bookings.md`). On astro specifically, SSR the catalog + detail pages
|
|
45
|
+
for SEO and run slots + form + book in a `client:only` island.
|
|
46
|
+
|
|
47
|
+
## 2. Shared booking state
|
|
48
|
+
|
|
49
|
+
Steps 1–4 build up one selection (this is exactly `BookParams` + `SelectedSlot`
|
|
50
|
+
in `../astro/templates/bookings/bookingDriver.ts`):
|
|
51
|
+
|
|
52
|
+
- **`service`** — the chosen service object (the booking step reads `service.payment` + `service.bookingPolicy`).
|
|
53
|
+
- **`slot`** — `{ serviceType, serviceId, localStartDate, localEndDate, timezone, scheduleId?, eventId?, locationId?, locationType? }`.
|
|
54
|
+
- **`formSubmission`** — the booking-form values, **keyed by each field's `target`** (§4).
|
|
55
|
+
|
|
56
|
+
Hold it however the framework prefers (React state lifted to a coordinator, a
|
|
57
|
+
store, route loaders + a provider, query-params). **Two rules that bite:**
|
|
58
|
+
|
|
59
|
+
1. **Persist the selection before navigating.** When a step lives on its own
|
|
60
|
+
route, write the selection into the shared state (or the URL) *before* moving
|
|
61
|
+
on, and let the next step **rehydrate from the slug/URL** on direct load /
|
|
62
|
+
refresh. A persistent provider that does not re-init on in-app navigation will
|
|
63
|
+
otherwise show an empty next step.
|
|
64
|
+
2. **The slug is the carry-across key.** `serviceSlug` (the service's
|
|
65
|
+
`mainSlug.name`) identifies the service across the slots + details steps;
|
|
66
|
+
resolve the full service from it when state is lost.
|
|
67
|
+
|
|
68
|
+
## 3. The booking SDK sequence — `bookingDriver.ts`
|
|
69
|
+
|
|
70
|
+
The whole booking — `createBooking → createCart → calculateCart → checkout-or-place`
|
|
71
|
+
— is in **`../astro/templates/bookings/bookingDriver.ts`**, in plain `@wix` SDK
|
|
72
|
+
calls (no React, no UI). **Use it as-is** (it is framework-agnostic); the booking
|
|
73
|
+
step calls `book(params)` then branches on the result. Do not re-author the
|
|
74
|
+
sequence — the payload shapes are exact and easy to get subtly wrong.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
book({ service, slot, formSubmission, timezone }) →
|
|
78
|
+
createBooking(...) // booking lands CREATED — the cart holds the seat
|
|
79
|
+
createCart(...) // one catalog item per bookingId, appId = BOOKING_APP_ID, channel WEB
|
|
80
|
+
calculateCart(cartId) // → { cart, summary }; totals on summary.priceSummary.total.amount
|
|
81
|
+
isCheckoutRequired(...) ?
|
|
82
|
+
→ { CheckoutRequired, cartId } // paid → navigateToCheckout(cartId, postFlowUrl) → Wix-hosted checkout
|
|
83
|
+
: placeOrder(cartId) → { CheckoutSkipped, orderId } // free / pay-in-person → confirmation
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Key facts the driver encodes (do not deviate):
|
|
87
|
+
- **No `confirmBooking`** — `confirmBooking` is the classic Bookings server-side
|
|
88
|
+
confirm step (it moves a booking from `CREATED` to `CONFIRMED`). Here the **ecom
|
|
89
|
+
cart holds the seat** instead — `placeOrder` (free/offline) or the hosted checkout
|
|
90
|
+
(paid) drives confirmation — so a client-only site completes the whole flow with no
|
|
91
|
+
server elevation.
|
|
92
|
+
- **ANY_RESOURCE fallback (staff)** — v1 has **no staff picker**, so `createBooking`
|
|
93
|
+
always sends `resourceSelections:[{ resourceTypeId:"1cd44cf8-…", selectionMethod:"ANY_RESOURCE" }]`
|
|
94
|
+
and Wix auto-assigns a bookable staff resource (appointment slots return
|
|
95
|
+
`availableResources:[]` yet book fine this way). (Explicit staff selection is a
|
|
96
|
+
fast-follow.)
|
|
97
|
+
- **Checkout decision** — `isCheckoutRequired`: if the service's booking policy charges
|
|
98
|
+
a **cancellation fee** (`service.bookingPolicy.cancellationFeePolicy.enabled`) →
|
|
99
|
+
checkout (a card must be on file); else total 0 → place; else `FULL_PAYMENT_OFFLINE`
|
|
100
|
+
→ place; else checkout.
|
|
101
|
+
- **Redirect shape** — paid bookings hand the cart to the Wix-hosted **ecom**
|
|
102
|
+
checkout: `createRedirectSession({ ecomCheckout: { checkoutId: cartId }, callbacks:{ postFlowUrl } })`.
|
|
103
|
+
|
|
104
|
+
### Browse + availability (the read calls the steps make)
|
|
105
|
+
|
|
106
|
+
Mirror what the Bookings SDK does (`services.queryServices` is the raw SDK, not a component):
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// Catalog (list) — the filter MUST include appId; pass conditionalFields, then .find():
|
|
110
|
+
services.queryServices({
|
|
111
|
+
query: { filter: { appId: BOOKING_APP_ID }, paging: { limit: 100 } },
|
|
112
|
+
conditionalFields: ["STAFF_MEMBER_DETAILS"],
|
|
113
|
+
}).find(); // → result.items ; filter out s.hidden
|
|
114
|
+
|
|
115
|
+
// Single service by slug — the .eq() builder chain:
|
|
116
|
+
services.queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
|
|
117
|
+
.eq("mainSlug.name", slug).eq("appId", BOOKING_APP_ID).limit(1).find();
|
|
118
|
+
|
|
119
|
+
// Availability (APPOINTMENT) — serviceId is a single GUID STRING:
|
|
120
|
+
availabilityTimeSlots.listAvailabilityTimeSlots({
|
|
121
|
+
serviceId, fromLocalDate, toLocalDate, timeZone, bookable: true, cursorPaging: { limit: 100 },
|
|
122
|
+
}); // slots: result.timeSlots[] (localStartDate/localEndDate/scheduleId at top level)
|
|
123
|
+
|
|
124
|
+
// Availability (CLASS) — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, no scheduleId:
|
|
125
|
+
eventTimeSlots.listEventTimeSlots({ serviceIds: [serviceId], fromLocalDate, toLocalDate, timeZone, includeNonBookable: false });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`fromLocalDate`/`toLocalDate` are **local** date strings `YYYY-MM-DDThh:mm:ss`
|
|
129
|
+
(no `Z`) with an explicit `timeZone`.
|
|
130
|
+
|
|
131
|
+
## 4. The booking form — schema-driven (`@wix/forms`)
|
|
132
|
+
|
|
133
|
+
The booking form is a **`@wix/forms` form** attached to the service
|
|
134
|
+
(`service.form._id`, namespace `wix.bookings.v2.bookings`). Render it
|
|
135
|
+
**schema-driven** — read the field list and render inputs by field type — the
|
|
136
|
+
**same renderer the forms vertical uses** (`../astro/forms/CONTACT_FORM.md`):
|
|
137
|
+
|
|
138
|
+
1. Fetch the form by `service.form._id` (`@wix/forms` `getForm`; server-side/elevated
|
|
139
|
+
where the framework allows). Read `form.formFields`.
|
|
140
|
+
2. Keep `fieldType === "INPUT" && !hidden` **and** a recognized string
|
|
141
|
+
`componentType` (`TEXT_INPUT` / `PHONE_INPUT` / `DROPDOWN`). For each, take
|
|
142
|
+
`inputOptions.target`, `inputOptions.required`, `inputOptions.stringOptions.componentType`,
|
|
143
|
+
and the label from `inputOptions.stringOptions.{textInputOptions|dropdownOptions|phoneInputOptions}.label`.
|
|
144
|
+
**Skip complex, object-valued fields** — e.g. a multi-line `ADDRESS` — they carry
|
|
145
|
+
no string `componentType`; rendering them as a text input sends a string and
|
|
146
|
+
`createBooking` rejects it with **"must be object"**. The booking enforces only
|
|
147
|
+
the contact basics (`first_name`/`last_name`/`email`), so omitting an optional
|
|
148
|
+
complex field is safe (do **not** default unknown field types to a text input).
|
|
149
|
+
3. Render generic inputs by `componentType`: `TEXT_INPUT` → text input,
|
|
150
|
+
`DROPDOWN` → select, `PHONE_INPUT` → `type=tel`; treat `identifier === "TEXT_AREA"`
|
|
151
|
+
(or `target` containing `message`) as a textarea (its `componentType` is still `TEXT_INPUT`).
|
|
152
|
+
4. Collect the values into an object **keyed by each field's `target`** — that is
|
|
153
|
+
`formSubmission`. Pass it to `book()`. Only include fields the visitor filled;
|
|
154
|
+
never send a value for a field you didn't render.
|
|
155
|
+
|
|
156
|
+
**Do not** submit `contactDetails`, and **do not** hardcode field names — key by
|
|
157
|
+
`target`. The default booking form's targets are snake_case `first_name` /
|
|
158
|
+
`last_name` / `email` / `phone`. Reference example: `../astro/templates/bookings/BookingForm.tsx`.
|
|
159
|
+
|
|
160
|
+
## 5. The day-calendar (slots step)
|
|
161
|
+
|
|
162
|
+
Show a **week calendar**, not a flat list of every slot: a 7-day strip with
|
|
163
|
+
week navigation → the picked day's times. A flat grid with time-only labels
|
|
164
|
+
leaves the visitor unable to tell which day a slot is on. Fetch availability for
|
|
165
|
+
the visible window (`fromLocalDate`/`toLocalDate` = the week bounds), group slots
|
|
166
|
+
by calendar day, and offer a **"check next availability"** action that probes
|
|
167
|
+
forward when a week is empty. Reference example:
|
|
168
|
+
`../astro/templates/bookings/AvailabilityCalendar.tsx`.
|
|
169
|
+
|
|
170
|
+
## 6. Client identity (per framework)
|
|
171
|
+
|
|
172
|
+
The SDK calls are identical everywhere; only how you get the client differs:
|
|
173
|
+
- **astro** — SSR read pages use the ambient `@wix/essentials` client
|
|
174
|
+
(`auth.elevate(services.queryServices)(...)`); browser islands call the `@wix`
|
|
175
|
+
modules **ambiently** too (the `@wix/astro` visitor client, like the ecom
|
|
176
|
+
`CartView` island) — no `createClient`/`OAuthStrategy`.
|
|
177
|
+
- **own / static** — acquire a visitor client once:
|
|
178
|
+
`createClient({ modules, auth: OAuthStrategy({ clientId: appId }) })` and call
|
|
179
|
+
the same functions on it. CDN imports for `none`, bundled for `own`.
|
|
180
|
+
|
|
181
|
+
## 7. Out of v1
|
|
182
|
+
|
|
183
|
+
Waitlist, on-site manage/cancel, location selector, staff filter, payment
|
|
184
|
+
breakdown, and CLASS sign-up are out of v1. Show bookable slots only;
|
|
185
|
+
post-booking self-service is handled by the Wix-hosted flow / member area.
|
|
@@ -1,106 +1,90 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bookings-implementer
|
|
3
|
-
description: "Implements Wix Bookings vertical — services catalog, service detail with
|
|
3
|
+
description: "Implements the Wix Bookings vertical — services catalog, a service detail page with a week-calendar availability picker, a schema-driven booking form, and confirmation. The booking runs client-side via the @wix SDK (ecom Cart V2 — no confirmBooking). Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Bookings Implementer
|
|
7
7
|
|
|
8
|
-
Extends `references/shared/IMPLEMENTER.md`. Read that file first for phase routing, REST auth + doc lookups,
|
|
8
|
+
Extends `references/shared/IMPLEMENTER.md`. Read that file first for phase routing, REST auth + doc lookups, the `.wix/seeded.json` read pattern, the return contract, style conventions, and common failure modes.
|
|
9
|
+
|
|
10
|
+
## The logic lives in one place
|
|
11
|
+
|
|
12
|
+
The booking **logic** — the step model, the shared selection state, the exact
|
|
13
|
+
`@wix` SDK sequence, and the schema-driven form — is framework-agnostic and lives
|
|
14
|
+
in **`./FLOW.md`**. Read it first. The astro guides below are the astro *wiring* of
|
|
15
|
+
that logic; the code examples under `<SKILL_ROOT>/references/astro/templates/bookings/`
|
|
16
|
+
are React (the astro islands use them directly). For an own/static build, the wiring guide is
|
|
17
|
+
`../custom/bookings/WIRING.md` (same logic, client-side `@wix/sdk`).
|
|
9
18
|
|
|
10
19
|
## Scope routing
|
|
11
20
|
|
|
12
21
|
| Scope | Phase | Reference |
|
|
13
22
|
|-------|-------|-----------|
|
|
14
|
-
| `seed` | Seed (
|
|
15
|
-
| `components` | Components
|
|
16
|
-
| `pages` | Pages
|
|
23
|
+
| `seed` | Seed — create services (and staff) via the Wix Bookings REST API | `./SERVICES_DATA.md` |
|
|
24
|
+
| `components` | Components — the client islands (week calendar, schema-driven form, the flow coordinator) | `./FLOW.md` + `../astro/bookings/COMPONENTS.md` |
|
|
25
|
+
| `pages` | Pages — `/services` listing, `/services/[slug]` detail, `/booking-confirmation`, + nav/home links | `./FLOW.md` + `../astro/bookings/SERVICES_PAGES.md` |
|
|
17
26
|
|
|
18
|
-
##
|
|
27
|
+
## Templates — read and adapt (don't invent)
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
Canonical examples live at `<SKILL_ROOT>/references/astro/templates/bookings/`.
|
|
30
|
+
Your `components` and `pages` scopes **read these and adapt them** — adapt brand
|
|
31
|
+
copy, headings, and styling; keep the SDK calls, payload shapes, and the data
|
|
32
|
+
flow (re-authoring the SDK wiring from scratch is the main source of API-shape bugs).
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
Components (`components` scope — TSX only, no CSS):
|
|
35
|
+
- `<SKILL_ROOT>/references/astro/templates/bookings/AvailabilityCalendar.tsx` — the week calendar (week strip → the day's slots; APPOINTMENT via `availabilityTimeSlots`, CLASS via `eventTimeSlots`).
|
|
36
|
+
- `<SKILL_ROOT>/references/astro/templates/bookings/BookingForm.tsx` — the schema-driven form (renders the `@wix/forms` field list, keys values by `target`) that drives `bookingDriver.book()`.
|
|
37
|
+
- `<SKILL_ROOT>/references/astro/templates/bookings/ServiceBookingFlow.tsx` — the coordinator island (holds the selected slot; calendar → form → redirect).
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- `src/pages/manage-booking.astro`
|
|
39
|
+
Pages (`pages` scope):
|
|
40
|
+
- `<SKILL_ROOT>/references/astro/templates/bookings/ServiceCard.astro`
|
|
41
|
+
- `<SKILL_ROOT>/references/astro/templates/bookings/services/index.astro`, `<SKILL_ROOT>/references/astro/templates/bookings/services/[slug].astro`
|
|
42
|
+
- `<SKILL_ROOT>/references/astro/templates/bookings/booking-confirmation.astro`
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
### Pre-copied by the orchestrator (do NOT write these yourself)
|
|
45
|
+
Mechanical, brand-agnostic — the orchestrator copies them before dispatch (BUILD-astro.md § build wave). Rely on them at the listed paths:
|
|
46
|
+
- `src/components/bookingDriver.ts` ← `<SKILL_ROOT>/references/astro/templates/bookings/bookingDriver.ts` — the booking SDK sequence (`book()`, `navigateToCheckout()`). The islands import it; never re-author it.
|
|
47
|
+
- `src/components/SeoTags.astro` ← `<SKILL_ROOT>/references/astro/templates/bookings/SeoTags.astro` — renders `service.seoData.tags`; imported by `services/[slug].astro`.
|
|
48
|
+
- `src/styles/components-bookings.css` ← `<SKILL_ROOT>/references/astro/templates/bookings/components-bookings.css` — the flow's component classes.
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
If a pre-copied file is missing at runtime, that's an orchestrator-side bug — return `status: "partial"` with `errors: [{code: "UTILITY_TEMPLATE_NOT_PRECOPIED", path: "<missing>"}]`; do not author your own version.
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
## Pre-return file-existence assertion (pages scope)
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
- `…/templates/bookings/ServiceBookingFlow.tsx` — coordinator (threads `serviceType`)
|
|
41
|
-
- `…/templates/bookings/ManageBooking.tsx` — cancel via anonymous token (used by `manage-booking.astro`)
|
|
54
|
+
Before returning `status: "complete"` from `pages`, verify on disk:
|
|
55
|
+
- `src/pages/services/index.astro`
|
|
56
|
+
- `src/pages/services/[slug].astro`
|
|
57
|
+
- `src/pages/booking-confirmation.astro`
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
- `…/templates/bookings/services/index.astro`, `…/services/[slug].astro`
|
|
45
|
-
- `…/templates/bookings/booking-confirmation.astro`, `…/manage-booking.astro`
|
|
59
|
+
If any declared file is missing, return `status: "partial"` with `errors: [{ code: "PHASE4_FILE_MISSING", path: "<expected path>" }]`.
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
These are mechanical (no brand content) — the orchestrator copies them before dispatch (BUILD-astro.md § Step 4.5). Just rely on them at the listed paths:
|
|
49
|
-
- `src/styles/components-bookings.css` ← `…/templates/bookings/components-bookings.css`
|
|
50
|
-
- `src/components/SeoTags.astro` ← `…/templates/bookings/SeoTags.astro` (renders `service.seoData.tags`; imported by `services/[slug].astro`)
|
|
51
|
-
- `src/pages/api/confirm-booking.ts` ← `…/templates/bookings/api/confirm-booking.ts` (elevated `confirmBooking` — holds the seat)
|
|
52
|
-
- `src/pages/api/waitlist.ts` ← `…/templates/bookings/api/waitlist.ts` (elevated native v1 waitlist register)
|
|
61
|
+
## Dependencies (Setup installs these — see SETUP.md)
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
`@wix/bookings @wix/essentials @wix/forms @wix/redirects @wix/auto_sdk_ecom_cart-v-2`.
|
|
64
|
+
`@wix/forms` renders the booking-form schema; `@wix/redirects` + `@wix/auto_sdk_ecom_cart-v-2` run the cart/checkout sequence.
|
|
55
65
|
|
|
56
66
|
## CSS ownership — bookings pack
|
|
57
67
|
|
|
58
|
-
Bookings-specific
|
|
59
|
-
|
|
60
|
-
- `.service-card`, `.service-card-image`, `.service-card-meta`, `.service-card-price` — the service card itself, including image containment, price badge, and duration tag.
|
|
61
|
-
- `.service-grid` — the layout that lists service cards on `/services`. Include `display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--spacing-xl);` plus View-Transitions opacity fade.
|
|
62
|
-
- `.availability-calendar` — the date-picker shell wrapping the calendar widget.
|
|
63
|
-
- `.time-slot`, `.time-slot--available`, `.time-slot--selected`, `.time-slot--full` (full CLASS session → waitlist), plus the `.time-slot-time` / `.time-slot-capacity` spans inside each slot button.
|
|
64
|
-
- `.booking-form` (+ `.booking-form-note`) — the booking/waitlist form revealed after a slot is selected.
|
|
65
|
-
- `.manage-booking`, `.manage-booking-summary`, `.manage-booking-status`, `.manage-booking-note`, `.manage-booking-link` — the `/manage-booking` view/cancel UI.
|
|
66
|
-
|
|
67
|
-
If `global.css` ships a partial rule for any class above, flag it in your return JSON's `errors` array (`{code: "GLOBAL_CSS_LEAK", class: "<name>"}`) and override with the complete rule in `components-bookings.css`.
|
|
68
|
+
Bookings-specific CSS lives in `src/styles/components-bookings.css` (pre-copied — see above), NOT in `global.css`. Classes the pack owns: `.service-card*`, `.service-grid`, `.availability-*`, `.time-slot*`, `.booking-*`. If `global.css` ships a partial rule for any of these, flag it (`{code:"GLOBAL_CSS_LEAK", class:"<name>"}`) and override in `components-bookings.css`.
|
|
68
69
|
|
|
69
70
|
## Bookings-specific failure modes
|
|
70
71
|
|
|
71
72
|
| Wrong | Right |
|
|
72
73
|
|-------|-------|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
| Read service slug from `service.slug` | V2
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
| Read duration from `service.schedule.sessions.duration` | V2 puts duration at `service.schedule.availabilityConstraints.sessionDurations[0]` (array of ints). |
|
|
91
|
-
| Read media URL from `service.info.images[0].url` | V2 media: `service.media.mainMedia.image.url`. |
|
|
92
|
-
| Use `import { availability } from '@wix/bookings'` | Use `import { availabilityTimeSlots } from '@wix/bookings'`. |
|
|
93
|
-
| Call `availability.queryAvailability` server-side on every render | Availability is real-time and session-specific — always call from the client-side React island, NOT from `.astro` frontmatter SSR. |
|
|
94
|
-
| Create a booking without `totalParticipants` | `createBooking` requires `totalParticipants: 1` (or `participantsChoices`). |
|
|
95
|
-
| Read booking ID from `result.booking.id` | The booking ID field is `result.booking._id` (underscore prefix). |
|
|
96
|
-
| Redirect to `/thank-you` (ecom route) after booking | Redirect to `/booking-confirmation?bookingId=<_id>`. The ecom thank-you page is unrelated to bookings. |
|
|
97
|
-
| Re-fetch the booking with `getBooking` from the confirmation page | Don't. Pass booking details (bookingId, startDate, service name) as URL query params during the client-side redirect after `createBooking` succeeds, and render from those. (A server re-fetch of someone else's booking is out of scope.) NB: this is **not** because `auth.elevate` is unavailable — see next row. |
|
|
98
|
-
| Assume `@wix/essentials` `auth.elevate` doesn't work in headless | It does. **Two different `elevate`s:** `@wix/essentials` `auth.elevate(fn)` works in `@wix/astro` SSR (it's how the listing/detail/CMS pages read data) — use it. What's unavailable is the **`wixClient.auth.elevate`** instance method on a hand-built `createClient({auth: OAuthStrategy})` (a Velo/Blocks API). Don't conflate them. |
|
|
99
|
-
| Mount `AvailabilityCalendar` without `client:only="react"` | Calendar must be fully client-side — SSR renders before timezone is known and breaks slot display. Always use `client:only="react"`. |
|
|
100
|
-
| Omit try/catch on `createBooking` | Booking creation can fail with 409 (slot taken) between slot fetch and submit. Catch and surface a friendly message; do not crash the page. |
|
|
101
|
-
| Use `service.id` to pass the service ID | The Wix SDK uses `_id` (with underscore) for all entity IDs. The service's ID is `service._id`. Using `service.id` will be `undefined`. |
|
|
102
|
-
| Read `resource._id` from `listAvailabilityTimeSlots` response | `availableResources` is EMPTY by default in list results. Omit `resource` from `createBooking` — Wix auto-assigns a resource during confirmation. |
|
|
103
|
-
| Use `timeSlot.slot.startDate` to read slot time | Slot fields are at the TOP LEVEL — there is no nested `slot` on the list response. APPOINTMENT slots: `timeSlot.localStartDate`, `timeSlot.localEndDate`, `timeSlot.scheduleId`. |
|
|
104
|
-
| Treat CLASS (event) slots like APPOINTMENT slots — guard clicks on `scheduleId`, book with `{scheduleId, startDate}` | CLASS event slots carry their session id at **`timeSlot.eventInfo.eventId`** and have **no `scheduleId`** (and no top-level `startDate`). Guarding the slot-click on `scheduleId` drops every CLASS click; the appointment-shaped `createBooking` payload doesn't apply. For CLASS: guard on `eventId`, and book with `bookedEntity.slot = { serviceId, eventId }` (Wix derives start/end/timezone/resource/location from the event). Keep `{ scheduleId, startDate, endDate, timezone, location }` for APPOINTMENT only. |
|
|
105
|
-
| Pass `hidden: true` services to the listing | Always filter with `{ hidden: false }` in your `queryServices` filter to exclude hidden services from the public listing. |
|
|
106
|
-
| Use `../../../` for imports in `[slug].astro` | `src/pages/services/[slug].astro` is two directories deep inside `src/`. Import Layout and components with `../../layouts/Layout.astro` and `../../components/…` — not three `../` which would escape `src/`. |
|
|
74
|
+
| Stop after `createBooking` returns | `createBooking` leaves the booking **`CREATED`**. The seat is held by the **ecom Cart V2**: continue `createCart → calculateCart → isCheckoutRequired ? hosted-checkout redirect : placeOrder`. The whole sequence is in `bookingDriver.ts` — use it. |
|
|
75
|
+
| Hardcode `selectedPaymentOption: "ONLINE"` on `createBooking` | **Derive** it from the service: `online && !inPerson → "ONLINE"`, `!online && inPerson → "OFFLINE"`, else `"ONLINE"`. A free / pay-in-person service is the only kind that reaches `placeOrder`, and booking it `ONLINE` makes the cart reject it with **`INSUFFICIENT_INVENTORY`** (`available_quantity: 0`). `bookingDriver.ts` derives this for you (don't pass `selectedPaymentOption` to `book()`). |
|
|
76
|
+
| Pass `contactDetails` to `createBooking` | Pass **`formSubmission`**, keyed by each field's **`target`** (default booking form: snake_case `first_name`/`last_name`/`email`/`phone`). `contactDetails` is what Wix derives back onto the response. |
|
|
77
|
+
| Hardcode the booking-form fields | Render the form **schema-driven**: read `service.form._id`'s fields via `@wix/forms`, render by `componentType`, key values by `target` (same renderer as `../astro/forms/CONTACT_FORM.md`). |
|
|
78
|
+
| Render every form field as a text input (default unknown types to `TEXT_INPUT`) | Render **only** fields with a recognized string `componentType` (`TEXT_INPUT`/`PHONE_INPUT`/`DROPDOWN`). **Skip complex, object-valued fields** (e.g. the default booking form's multi-line `ADDRESS`) — sending a string for them fails `createBooking` with **"must be object"**. Only `first_name`/`last_name`/`email` are enforced, so skipping optional complex fields is safe. |
|
|
79
|
+
| Call `queryServices` without an `appId` filter | Always include `appId` (`13d21c63-…`) in the filter. List: `queryServices({ query:{ filter:{ appId }, paging }, conditionalFields:["STAFF_MEMBER_DETAILS"] }).find()`. Single: the `.eq("mainSlug.name", slug).eq("appId", …).limit(1).find()` builder chain. |
|
|
80
|
+
| Build `createClient({ auth: OAuthStrategy({ clientId: import.meta.env.* }) })` in `.astro` SSR | The `*_WIX_CLIENT_ID` env var is client-only → `undefined` at server render → 500. SSR reads use the ambient `@wix/essentials`: `auth.elevate(services.queryServices)(...)`. |
|
|
81
|
+
| Build an `OAuthStrategy` client inside an astro browser island | Astro islands call the `@wix` modules **ambiently** (the `@wix/astro` visitor client, like the ecom `CartView`) — no `createClient`. (Own/static builds DO use `OAuthStrategy` — `../custom/bookings/WIRING.md`.) |
|
|
82
|
+
| Pass UTC ISO strings to `listAvailabilityTimeSlots` | Use **local** date strings `YYYY-MM-DDThh:mm:ss` (no `Z`) + a separate `timeZone`. |
|
|
83
|
+
| `availability.queryAvailability` / `import { availability }` | APPOINTMENT → `availabilityTimeSlots.listAvailabilityTimeSlots({ serviceId: <string> })`; CLASS → `eventTimeSlots.listEventTimeSlots({ serviceIds: [<string>] })`. The `availability` namespace does not exist. |
|
|
84
|
+
| Read slot time from `timeSlot.slot.startDate` | Slot fields are at the **top level**: `localStartDate`, `localEndDate`, `scheduleId` (APPOINTMENT). CLASS slots carry `eventInfo.eventId` and have **no** `scheduleId`. |
|
|
85
|
+
| Dump every slot in one flat, time-only grid | Show a **week calendar**: a day strip → the picked day's times. A flat grid hides which day a slot is on. |
|
|
86
|
+
| Read service slug from `service.slug` / name from `service.info.name` | V2 is flat: slug `service.mainSlug.name` (fallback `supportedSlugs[0].name`), `service.name`, `service.description`, `service.tagLine`; duration `service.schedule.availabilityConstraints.sessionDurations[0]`; price `service.payment.fixed.price.value` (string); media `service.media.mainMedia.image.url`. |
|
|
87
|
+
| Use `service.id` / `result.booking.id` | Entity ids are `_id` (underscore): `service._id`, `booking._id`. |
|
|
88
|
+
| Mount the calendar/form without `client:only="react"` | Availability + booking are timezone/session-specific — always `client:only="react"`. SSR only the read pages (catalog/detail) for SEO. |
|
|
89
|
+
| Omit try/catch on the booking | `createBooking` can reject (e.g. a slot taken between fetch and submit, or strict phone validation). Catch and surface a friendly message; don't crash. |
|
|
90
|
+
| Ship a location selector, staff filter, payment breakdown, CLASS sign-up, or waitlist | Out of v1 — appointment booking only. |
|