@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
|
@@ -1,337 +1,45 @@
|
|
|
1
|
-
# Bookings Components —
|
|
1
|
+
# Bookings Components — the client islands (astro)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The `components` scope of the bookings vertical. You write the React islands that
|
|
4
|
+
power the booking UI. The **logic** is in `../../bookings/FLOW.md` (read it first);
|
|
5
|
+
this doc is the astro wiring + the gotchas. `src/styles/components-bookings.css`
|
|
6
|
+
and `src/components/bookingDriver.ts` are pre-copied — do **not** write them.
|
|
6
7
|
|
|
7
8
|
Read `references/shared/IMPLEMENTER.md` + `references/shared/STYLING.md` first.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- Content region (`class="service-card-meta"`):
|
|
46
|
-
- `<h3 class="service-card-name">{name}</h3>`
|
|
47
|
-
- `{tagLine && <p class="service-card-tagline">{tagLine}</p>}`
|
|
48
|
-
- Meta row: duration badge + price (when present) in a `<div class="service-card-price">`:
|
|
49
|
-
- Duration: `<span>{durationMinutes} min</span>`
|
|
50
|
-
- Price: `<span>{price}</span>` when price is defined
|
|
51
|
-
- No price text when price is absent (service may be free or "Contact for pricing")
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## AvailabilityCalendar.tsx
|
|
56
|
-
|
|
57
|
-
A `client:only="react"` React island. Fetches and displays available slots for a specific service.
|
|
58
|
-
|
|
59
|
-
**Props:**
|
|
60
|
-
```typescript
|
|
61
|
-
interface AvailabilityCalendarProps {
|
|
62
|
-
serviceId: string;
|
|
63
|
-
serviceName: string;
|
|
64
|
-
serviceType: 'APPOINTMENT' | 'CLASS'; // drives which time-slots API + slot shape
|
|
65
|
-
onSlotSelected: (slot: SelectedSlot) => void;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// APPOINTMENT and CLASS slots have DIFFERENT shapes — see "SDK wiring" below.
|
|
69
|
-
// APPOINTMENT slots carry `scheduleId` + `startDate`; CLASS (event) slots carry
|
|
70
|
-
// `eventInfo.eventId` and have NO `scheduleId`. Carry both optionally + the type.
|
|
71
|
-
interface SelectedSlot {
|
|
72
|
-
serviceType: 'APPOINTMENT' | 'CLASS';
|
|
73
|
-
localStartDate: string; // YYYY-MM-DDThh:mm:ss — from slot.localStartDate
|
|
74
|
-
localEndDate: string; // YYYY-MM-DDThh:mm:ss — from slot.localEndDate
|
|
75
|
-
serviceId: string;
|
|
76
|
-
timezone: string; // result.timeZone from the list response
|
|
77
|
-
// APPOINTMENT only:
|
|
78
|
-
scheduleId?: string; // timeSlot.scheduleId — undefined for CLASS
|
|
79
|
-
locationId?: string; // timeSlot.location.id (BUSINESS locations only)
|
|
80
|
-
locationType?: string; // timeSlot.location.locationType
|
|
81
|
-
// CLASS only:
|
|
82
|
-
eventId?: string; // timeSlot.eventInfo.eventId — undefined for APPOINTMENT
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**Behavior:**
|
|
87
|
-
1. On mount, default to today's date. Fetch available slots for today via `fetchSlots(serviceId, serviceType, today)` (which picks `listAvailabilityTimeSlots` for APPOINTMENT or `listEventTimeSlots` for CLASS — see SDK wiring below).
|
|
88
|
-
2. Render a simple date-navigation header (← Prev / date display / Next →) with `<button>` controls. Move by ±1 day. No full calendar grid — a date navigator is sufficient.
|
|
89
|
-
3. Below the date navigator, render the available slots as a grid of `<button>` elements (`className="time-slot time-slot--available"`). On click, mark the slot as selected (`className="time-slot time-slot--selected"`) and call `onSlotSelected(slot)`. **Guard the click on the type-appropriate id** — `slot.localStartDate && (slot.serviceType === 'CLASS' ? slot.eventId : slot.scheduleId)`. Guarding on `scheduleId` alone silently drops every CLASS click (event slots have no `scheduleId`), so the booking form never appears.
|
|
90
|
-
4. If no slots are available for the selected date, display: `<p className="availability-empty">No availability on this date — try another day.</p>`.
|
|
91
|
-
5. Loading state: display `<p className="availability-loading">Checking availability…</p>` while fetching.
|
|
92
|
-
6. Error state: display `<p className="availability-error">Could not load availability — please try again.</p>` and a "Retry" button.
|
|
93
|
-
|
|
94
|
-
> **`.tsx` files use `className=`, NOT `class=`.** React JSX requires the `className` attribute on intrinsic elements; using `class=` produces a `tsc` error per occurrence (`Type '{ class: string; ...}' is not assignable to type 'DetailedHTMLProps<...>'`). Only `.astro` files (like `ServiceCard.astro` above) use `class=`. The same rule applies to `BookingForm.tsx` and `ServiceBookingFlow.tsx`.
|
|
95
|
-
|
|
96
|
-
**SDK wiring:**
|
|
97
|
-
|
|
98
|
-
> **Two different namespaces, one per service type** — both are real `@wix/bookings` exports (`availability` is not):
|
|
99
|
-
> - **APPOINTMENT** → `availabilityTimeSlots.listAvailabilityTimeSlots({ serviceId: [id], … bookable: true })`. Slots carry `scheduleId` + `startDate` at the top level.
|
|
100
|
-
> - **CLASS** → `eventTimeSlots.listEventTimeSlots({ serviceIds: [id], … includeNonBookable: false })`. Note the **different namespace** (`eventTimeSlots`, not `availabilityTimeSlots`), the **plural `serviceIds`**, and `includeNonBookable` (not `bookable`). Class slots carry their session id at **`eventInfo.eventId`** and have **no `scheduleId`** (and no top-level `startDate`).
|
|
101
|
-
>
|
|
102
|
-
> Branch on the `serviceType` prop. For CLASS the calendar is empty until the merchant schedules sessions for the class (see SERVICES_DATA.md § "CLASS session gap") — an empty state there is expected, not a bug.
|
|
103
|
-
|
|
104
|
-
> **Client ID in browser islands comes from `astro:env/client`, NOT `import.meta.env`.** In `@wix/astro` the client ID is exposed to the browser through the `astro:env/client` virtual module: `import { WIX_CLIENT_ID } from "astro:env/client"`. `import.meta.env.WIX_CLIENT_ID` (and `PUBLIC_WIX_CLIENT_ID`) is **`undefined` in the browser bundle**, so `OAuthStrategy` POSTs an empty `client_id` and every client-side SDK call fails at `POST https://www.wixapis.com/oauth2/token` with `400 {"error":"invalid_request"}`. (The var is `WIX_CLIENT_ID` — no `PUBLIC_` prefix.)
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
import { createClient, OAuthStrategy } from '@wix/sdk';
|
|
108
|
-
import { availabilityTimeSlots, eventTimeSlots, bookings } from '@wix/bookings';
|
|
109
|
-
import { WIX_CLIENT_ID } from 'astro:env/client';
|
|
110
|
-
|
|
111
|
-
const wixClient = createClient({
|
|
112
|
-
modules: { availabilityTimeSlots, eventTimeSlots, bookings },
|
|
113
|
-
auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Local date strings (YYYY-MM-DDThh:mm:ss) — NOT ISO UTC strings with Z. The API
|
|
117
|
-
// expects local time in the business timezone, alongside an explicit timeZone.
|
|
118
|
-
const pad = (n: number) => String(n).padStart(2, '0');
|
|
119
|
-
const localDate = (d: Date) =>
|
|
120
|
-
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
121
|
-
|
|
122
|
-
// Returns SelectedSlot[] normalized across both service types so the rest of the
|
|
123
|
-
// component (and BookingForm) never re-branches on raw API shapes.
|
|
124
|
-
const fetchSlots = async (
|
|
125
|
-
serviceId: string,
|
|
126
|
-
serviceType: 'APPOINTMENT' | 'CLASS',
|
|
127
|
-
date: Date,
|
|
128
|
-
): Promise<{ slots: SelectedSlot[]; timezone: string }> => {
|
|
129
|
-
const start = new Date(date); start.setHours(0, 0, 0, 0);
|
|
130
|
-
const end = new Date(date); end.setHours(23, 59, 59, 0);
|
|
131
|
-
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
132
|
-
|
|
133
|
-
if (serviceType === 'CLASS') {
|
|
134
|
-
const result = await wixClient.eventTimeSlots.listEventTimeSlots({
|
|
135
|
-
serviceIds: [serviceId], // plural — CLASS API
|
|
136
|
-
fromLocalDate: localDate(start),
|
|
137
|
-
toLocalDate: localDate(end),
|
|
138
|
-
timeZone,
|
|
139
|
-
includeNonBookable: false,
|
|
140
|
-
});
|
|
141
|
-
const tz = result.timeZone ?? timeZone;
|
|
142
|
-
const slots: SelectedSlot[] = (result.timeSlots ?? []).map((ts: any) => ({
|
|
143
|
-
serviceType: 'CLASS',
|
|
144
|
-
serviceId,
|
|
145
|
-
localStartDate: ts.localStartDate,
|
|
146
|
-
localEndDate: ts.localEndDate,
|
|
147
|
-
eventId: ts.eventInfo?.eventId, // session id lives here — there is NO scheduleId
|
|
148
|
-
timezone: tz,
|
|
149
|
-
}));
|
|
150
|
-
return { slots, timezone: tz };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// APPOINTMENT
|
|
154
|
-
const result = await wixClient.availabilityTimeSlots.listAvailabilityTimeSlots({
|
|
155
|
-
serviceId, // a single GUID STRING — NOT an array (the array form is the CLASS `serviceIds`)
|
|
156
|
-
fromLocalDate: localDate(start),
|
|
157
|
-
toLocalDate: localDate(end),
|
|
158
|
-
timeZone,
|
|
159
|
-
bookable: true,
|
|
160
|
-
cursorPaging: { limit: 50 },
|
|
161
|
-
});
|
|
162
|
-
const tz = result.timeZone ?? timeZone;
|
|
163
|
-
const slots: SelectedSlot[] = (result.timeSlots ?? []).map((ts: any) => ({
|
|
164
|
-
serviceType: 'APPOINTMENT',
|
|
165
|
-
serviceId,
|
|
166
|
-
localStartDate: ts.localStartDate,
|
|
167
|
-
localEndDate: ts.localEndDate,
|
|
168
|
-
scheduleId: ts.scheduleId,
|
|
169
|
-
locationId: ts.location?.id,
|
|
170
|
-
locationType: ts.location?.locationType,
|
|
171
|
-
timezone: tz,
|
|
172
|
-
}));
|
|
173
|
-
return { slots, timezone: tz };
|
|
174
|
-
};
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Slot fields differ by type — both have `localStartDate`/`localEndDate` at the TOP LEVEL (not nested under a `slot` property):
|
|
178
|
-
- **APPOINTMENT** (`listAvailabilityTimeSlots`): `timeSlot.scheduleId`, `timeSlot.serviceId`, `timeSlot.location.{id,locationType}`, `timeSlot.bookable`. `timeSlot.availableResources` is **EMPTY by default** — do NOT read a resource ID here.
|
|
179
|
-
- **CLASS** (`listEventTimeSlots`): `timeSlot.eventInfo.eventId` (the session id), `timeSlot.totalCapacity`/`remainingCapacity`. **No `scheduleId`, no top-level `startDate`.**
|
|
180
|
-
|
|
181
|
-
> **Do NOT try to read `resource._id` from list results.** Omit `resource` from the `createBooking` payload — Wix auto-assigns an available resource during booking confirmation. You only need resource details if you want to show "book with specific staff" UI (advanced use case).
|
|
182
|
-
|
|
183
|
-
Format slot times for display: `new Date(timeSlot.localStartDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })`.
|
|
184
|
-
|
|
185
|
-
`SelectedSlot` (defined once at the top of this component) carries the info needed for booking — including `serviceType` and, for CLASS, `eventId` instead of `scheduleId`. `BookingForm` reads `serviceType` to choose the `createBooking` payload shape.
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
## BookingForm.tsx
|
|
190
|
-
|
|
191
|
-
A `client:only="react"` React island. Renders after the user selects a slot.
|
|
192
|
-
|
|
193
|
-
**Props:**
|
|
194
|
-
```typescript
|
|
195
|
-
interface BookingFormProps {
|
|
196
|
-
serviceId: string;
|
|
197
|
-
serviceName: string;
|
|
198
|
-
serviceType: 'APPOINTMENT' | 'CLASS'; // selects the createBooking slot shape
|
|
199
|
-
slot: SelectedSlot;
|
|
200
|
-
onSuccess: (bookingId: string, startDate: string) => void; // startDate for confirmation page URL
|
|
201
|
-
onCancel: () => void;
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
**Fields:**
|
|
206
|
-
- First name (required)
|
|
207
|
-
- Last name (optional)
|
|
208
|
-
- Email (required)
|
|
209
|
-
- Phone (optional)
|
|
210
|
-
- Submit button: "Confirm Booking"
|
|
211
|
-
- Cancel link that calls `onCancel()`
|
|
212
|
-
|
|
213
|
-
**On submit:**
|
|
214
|
-
1. Validate email + first name are non-empty.
|
|
215
|
-
2. Call `bookings.createBooking(...)`:
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
import { createClient, OAuthStrategy } from '@wix/sdk';
|
|
219
|
-
import { bookings } from '@wix/bookings';
|
|
220
|
-
import { WIX_CLIENT_ID } from 'astro:env/client'; // browser client ID — NOT import.meta.env (see Bug-10 note)
|
|
221
|
-
|
|
222
|
-
const wixClient = createClient({
|
|
223
|
-
modules: { bookings },
|
|
224
|
-
auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// The slot shape differs by service type — build bookedEntity.slot accordingly.
|
|
228
|
-
// Omit `resource` either way — Wix auto-assigns an available resource on confirm.
|
|
229
|
-
const slot = props.slot.serviceType === 'CLASS'
|
|
230
|
-
// CLASS: a class session is identified by eventId. Wix derives start/end/
|
|
231
|
-
// timezone/resource/location from the event — do NOT send scheduleId/startDate.
|
|
232
|
-
? {
|
|
233
|
-
serviceId: props.slot.serviceId,
|
|
234
|
-
eventId: props.slot.eventId,
|
|
235
|
-
}
|
|
236
|
-
// APPOINTMENT: identified by scheduleId + explicit start/end/timezone/location.
|
|
237
|
-
: {
|
|
238
|
-
serviceId: props.slot.serviceId,
|
|
239
|
-
scheduleId: props.slot.scheduleId,
|
|
240
|
-
startDate: props.slot.localStartDate, // pass localStartDate as startDate
|
|
241
|
-
endDate: props.slot.localEndDate, // pass localEndDate as endDate
|
|
242
|
-
timezone: props.slot.timezone,
|
|
243
|
-
...(props.slot.locationId
|
|
244
|
-
? { location: { _id: props.slot.locationId, locationType: props.slot.locationType as any } }
|
|
245
|
-
: { location: { locationType: 'OWNER_BUSINESS' as const } }),
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const bookingPayload = {
|
|
249
|
-
totalParticipants: 1,
|
|
250
|
-
contactDetails: {
|
|
251
|
-
firstName: formData.firstName,
|
|
252
|
-
lastName: formData.lastName || undefined,
|
|
253
|
-
email: formData.email,
|
|
254
|
-
phone: formData.phone || undefined,
|
|
255
|
-
},
|
|
256
|
-
selectedPaymentOption: 'OFFLINE' as const, // "OFFLINE" = pay at session; use "ONLINE" if the service has online payment enabled
|
|
257
|
-
bookedEntity: { slot },
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const result = await wixClient.bookings.createBooking(bookingPayload);
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
3. **Confirm the booking — this is mandatory, not optional.** `createBooking` returns a booking in **`CREATED`** status, which **does not hold a seat or block availability** (only `CONFIRMED` does — [lifecycle docs](https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/introduction)). If you stop at `createBooking`, the class **overbooks** and `remainingCapacity` never drops. Confirm via a server endpoint (next section) — `confirmBooking` needs Manage Bookings, so it can't run from this visitor client.
|
|
264
|
-
4. On success (confirm returns ok): call `onSuccess(result.booking?._id ?? '', props.slot.localStartDate)`. The booking ID field is `_id` (underscore prefix), not `id`.
|
|
265
|
-
5. On `SESSION_CAPACITY_EXCEEDED` (full session): route to the **waitlist** (see "Capacity, instructor & waitlist" below).
|
|
266
|
-
6. On other errors: `<p className="booking-error">This time slot was just taken. Please select another.</p>` + `onCancel()`. Loading state "Confirming…" while in flight. Always wrap in try/catch.
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
## Booking lifecycle, capacity, instructor & waitlist (verified live)
|
|
271
|
-
|
|
272
|
-
These four behaviors were all verified against a live Bookings site — they're easy to get wrong and pass `astro build` while breaking at runtime.
|
|
273
|
-
|
|
274
|
-
### Confirm step (overbooking guard) — server endpoint `src/pages/api/confirm-booking.ts`
|
|
275
|
-
`createBooking` → `CREATED` (holds nothing). Confirm it server-side to occupy the seat:
|
|
276
|
-
```typescript
|
|
277
|
-
// POST { bookingId, revision } — elevated (Manage Bookings).
|
|
278
|
-
import { auth } from "@wix/essentials";
|
|
279
|
-
import { bookings } from "@wix/bookings";
|
|
280
|
-
const res = await auth.elevate(bookings.confirmBooking)(bookingId, String(revision), {
|
|
281
|
-
paymentStatus: "NOT_PAID", // pay-on-site class
|
|
282
|
-
flowControlSettings: { checkAvailabilityValidation: true }, // 409s if it just filled — the real overbooking guard
|
|
283
|
-
});
|
|
284
|
-
```
|
|
285
|
-
`confirmBooking` is a real SDK method, so `auth.elevate(bookings.confirmBooking)` elevates correctly. The island POSTs `{ bookingId, revision }` here right after `createBooking`, and only redirects to the confirmation page when this returns ok.
|
|
286
|
-
|
|
287
|
-
### Capacity (multi-participant)
|
|
288
|
-
The list slot carries `totalCapacity` / `remainingCapacity` / **`bookableCapacity`** (use `bookableCapacity` — it accounts for waitlist holds). Show "N spots left"; render a "Number of spots" selector capped at `bookableCapacity` and pass it as `booking.totalParticipants` (the verified field; not `numberOfParticipants`). A slot with `bookableCapacity <= 0` is full → waitlist.
|
|
289
|
-
|
|
290
|
-
### Instructor (per CLASS session)
|
|
291
|
-
`listEventTimeSlots` does **not** return resources. Resolve the instructor for a chosen slot via `eventTimeSlots.getEventTimeSlot(eventId)` → `timeSlot.availableResources[].resources[].name` (take the first). Display it on the form ("with Jordan Rivera").
|
|
292
|
-
|
|
293
|
-
### Waitlist (native, v1 — there is no v2/v3 waitlist)
|
|
294
|
-
A full session's `createBooking` throws `SESSION_CAPACITY_EXCEEDED`, whose message contains the **v1 session id** (*"…on session `<id>`"*). Extract it and POST to a server endpoint that registers on the native waitlist (`POST /bookings/v1/waitlist/register`, needs `contactId` + Manage Bookings → elevated). See the SSR-endpoints section in `SERVICES_PAGES.md`. Front-end extraction:
|
|
295
|
-
```typescript
|
|
296
|
-
const full = `${err?.message ?? ""} ${JSON.stringify(err?.details ?? {})} ${String(err)}`;
|
|
297
|
-
const sessionId = full.match(/on session ([^\s"}]+)/)?.[1]; // → waitingResource
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
---
|
|
301
|
-
|
|
302
|
-
## Wiring note — how the page uses these islands
|
|
303
|
-
|
|
304
|
-
The `/services/[slug].astro` page (written by the `pages` scope) mounts both islands in a coordinated flow:
|
|
305
|
-
|
|
306
|
-
```tsx
|
|
307
|
-
// Pseudocode — actual wiring is done by the pages scope.
|
|
308
|
-
// `serviceType` (from service.type) MUST be threaded through so the calendar
|
|
309
|
-
// picks the right time-slots API and the form builds the right createBooking shape.
|
|
310
|
-
const [selectedSlot, setSelectedSlot] = useState(null);
|
|
311
|
-
|
|
312
|
-
if (!selectedSlot) {
|
|
313
|
-
return <AvailabilityCalendar serviceId={id} serviceType={type} onSlotSelected={setSelectedSlot} />;
|
|
314
|
-
}
|
|
315
|
-
return <BookingForm serviceId={id} serviceType={type} slot={selectedSlot} onSuccess={handleSuccess} onCancel={() => setSelectedSlot(null)} />;
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
The components need to export cleanly so the page's React coordinator island can import both. Export each as a default export from its file.
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## Return contract
|
|
323
|
-
|
|
324
|
-
```json
|
|
325
|
-
{
|
|
326
|
-
"status": "complete",
|
|
327
|
-
"phase": "components",
|
|
328
|
-
"vertical": "bookings",
|
|
329
|
-
"files": [
|
|
330
|
-
"src/components/ServiceCard.astro",
|
|
331
|
-
"src/components/AvailabilityCalendar.tsx",
|
|
332
|
-
"src/components/BookingForm.tsx",
|
|
333
|
-
"src/components/ServiceBookingFlow.tsx",
|
|
334
|
-
"src/components/ManageBooking.tsx"
|
|
335
|
-
]
|
|
336
|
-
}
|
|
337
|
-
```
|
|
10
|
+
> **Start from the templates — don't re-author the SDK wiring.** Each island has
|
|
11
|
+
> an example at `<SKILL_ROOT>/references/astro/templates/bookings/`. Read and adapt
|
|
12
|
+
> (brand copy, styling, class names already in `components-bookings.css`); keep the
|
|
13
|
+
> SDK calls + payload shapes.
|
|
14
|
+
|
|
15
|
+
## Islands you write
|
|
16
|
+
|
|
17
|
+
| File | From template | Role |
|
|
18
|
+
|------|---------------|------|
|
|
19
|
+
| `src/components/ServiceBookingFlow.tsx` | `…/ServiceBookingFlow.tsx` | `client:only` coordinator — holds the selected slot, swaps calendar → form, redirects on success. Mounted by `services/[slug].astro`. |
|
|
20
|
+
| `src/components/AvailabilityCalendar.tsx` | `…/AvailabilityCalendar.tsx` | the **week calendar** — week strip + nav → the picked day's slots. APPOINTMENT → `availabilityTimeSlots`; CLASS → `eventTimeSlots`. |
|
|
21
|
+
| `src/components/BookingForm.tsx` | `…/BookingForm.tsx` | the **schema-driven** form — renders the `@wix/forms` field list (passed in from the SSR page), keys values by `target`, calls `bookingDriver.book()`. |
|
|
22
|
+
|
|
23
|
+
## astro-specific rules
|
|
24
|
+
|
|
25
|
+
1. **Islands call `@wix` ambiently — no `createClient`/`OAuthStrategy`.** In an
|
|
26
|
+
astro browser island the `@wix/astro` visitor client is ambient (the same way
|
|
27
|
+
the ecom `CartView` island works): `import { availabilityTimeSlots } from "@wix/bookings"`
|
|
28
|
+
and call it directly. Do **not** build an `OAuthStrategy` client or read
|
|
29
|
+
`WIX_CLIENT_ID` — that pattern is for own/static builds (`../../custom/bookings/WIRING.md`).
|
|
30
|
+
2. **The booking sequence is imported, not re-authored.** Import `book`,
|
|
31
|
+
`navigateToCheckout`, `BookResultType`, and the `SelectedSlot` type from the
|
|
32
|
+
pre-copied `./bookingDriver`. The form island calls `book(...)` and branches:
|
|
33
|
+
`CheckoutRequired` → `navigateToCheckout(cartId, postFlowUrl)`; `CheckoutSkipped`
|
|
34
|
+
→ redirect to `/booking-confirmation`.
|
|
35
|
+
3. **Mount everything `client:only="react"`.** Availability + booking are
|
|
36
|
+
timezone/session-specific; never SSR them.
|
|
37
|
+
4. **The form fields come from SSR.** `services/[slug].astro` fetches the booking
|
|
38
|
+
form schema and passes a `fields` array into `ServiceBookingFlow` → `BookingForm`
|
|
39
|
+
(see `SERVICES_PAGES.md`). The island renders the fields generically — it does
|
|
40
|
+
not fetch the schema itself.
|
|
41
|
+
5. **No CSS.** The classes (`.availability-*`, `.time-slot*`, `.booking-*`) are in
|
|
42
|
+
the pre-copied `components-bookings.css`; reference them, don't write them.
|
|
43
|
+
|
|
44
|
+
## Return
|
|
45
|
+
`{ status, phase, scope: "components", summary, data, files, errors }`. If a pre-copied dependency (`bookingDriver.ts`, `components-bookings.css`) is missing, return `status: "partial"` with the coded error.
|