@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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +2 -1
  3. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +14 -0
  4. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +11 -6
  5. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +3 -17
  6. package/skills/wix-app/references/editor-react-component/SSR.md +119 -0
  7. package/skills/wix-design-system/SKILL.md +1 -1
  8. package/skills/wix-headless/references/BUILD-astro.md +5 -7
  9. package/skills/wix-headless/references/SETUP.md +1 -1
  10. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
  11. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +58 -428
  12. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +4 -7
  13. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +158 -143
  14. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
  15. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +17 -13
  16. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +16 -78
  17. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
  18. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +122 -179
  19. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +61 -9
  20. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +10 -2
  21. package/skills/wix-headless/references/bookings/FLOW.md +185 -0
  22. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +60 -76
  23. package/skills/wix-headless/references/custom/bookings/WIRING.md +123 -65
  24. package/skills/wix-headless/references/verticals/bookings.md +6 -8
  25. package/skills/wix-manage/SKILL.md +12 -0
  26. package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
  27. package/skills/wix-manage/references/bookings/create-appointment-service.md +266 -0
  28. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
  29. package/skills/wix-manage/references/bookings/create-class-service.md +245 -0
  30. package/skills/wix-manage/references/bookings/create-course-service.md +249 -0
  31. package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
  32. package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
  33. package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
  34. package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
  35. package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
  36. package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
  37. package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
@@ -1,194 +1,136 @@
1
1
  import { useState } from "react";
2
- import { createClient, OAuthStrategy } from "@wix/sdk";
3
- import { bookings } from "@wix/bookings";
4
- import type { SelectedSlot } from "./AvailabilityCalendar";
5
- import { WIX_CLIENT_ID } from "astro:env/client"; // browser client ID, NOT import.meta.env
6
-
7
- // BookingForm.tsx — client:only="react" island. Renders after a slot is picked.
8
- // Flow: createBooking → CREATED holds NO seat, so POST the booking to
9
- // the elevated /api/confirm-booking to occupy it (and validate availability).
10
- // A full CLASS session throws SESSION_CAPACITY_EXCEEDED whose message carries the
11
- // v1 session id → register on the native waitlist via /api/waitlist.
2
+ import { book, navigateToCheckout, BookResultType } from "./bookingDriver";
3
+ import type { SelectedSlot } from "./bookingDriver";
4
+
5
+ // BookingForm.tsx — client:only="react" island. Renders the service's booking
6
+ // form SCHEMA (from @wix/forms, fetched server-side and passed in as `fields`),
7
+ // collects the values keyed by each field's `target`, and drives the booking
8
+ // sequence in bookingDriver.book(). This is the same schema-driven renderer the
9
+ // forms vertical uses (references/astro/forms/CONTACT_FORM.md) — generic inputs
10
+ // chosen by `componentType` — so it adapts to whatever fields the booking form
11
+ // defines, rather than hardcoding first/last/email/phone.
12
+ //
13
+ // On submit: book() returns CheckoutRequired (paid → hand off to the Wix-hosted
14
+ // checkout) or CheckoutSkipped (free / pay-in-person → straight to confirmation).
15
+
16
+ export interface BookingFormField {
17
+ label: string;
18
+ target: string; // the key createBooking expects in formSubmission
19
+ required: boolean;
20
+ componentType: string; // "TEXT_INPUT" | "DROPDOWN" | "PHONE_INPUT"
21
+ identifier?: string;
22
+ options?: { value: string; label: string }[];
23
+ }
12
24
 
13
25
  interface Props {
14
- serviceId: string;
26
+ service: any; // SSR-fetched service — bookingDriver reads payment + bookingPolicy
15
27
  serviceName: string;
16
- serviceType: "APPOINTMENT" | "CLASS";
17
28
  slot: SelectedSlot;
18
- onSuccess: (bookingId: string, startDate: string) => void;
29
+ fields: BookingFormField[];
30
+ onSuccess: (id: string, startDate?: string) => void;
19
31
  onCancel: () => void;
20
32
  }
21
33
 
22
- const wixClient = createClient({
23
- modules: { bookings },
24
- auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
25
- });
26
-
27
- const slotDisplay = (slot: SelectedSlot) =>
28
- slot.localStartDate
29
- ? new Date(slot.localStartDate).toLocaleString([], {
30
- weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit",
31
- })
32
- : "";
33
-
34
- export default function BookingForm({ serviceName, serviceType, slot, onSuccess, onCancel }: Props) {
35
- const [firstName, setFirstName] = useState("");
36
- const [lastName, setLastName] = useState("");
37
- const [email, setEmail] = useState("");
38
- const [phone, setPhone] = useState("");
39
- const [participants, setParticipants] = useState(1);
40
- const [submitting, setSubmitting] = useState(false);
34
+ const friendlyError = (err: any): string => {
35
+ // Surface field validation (e.g. a malformed phone number) rather than crashing.
36
+ const violations = err?.details?.validationError?.fieldViolations ?? [];
37
+ const first = violations[0]?.description ?? violations[0]?.data?.errors?.[0]?.errorMessage;
38
+ if (first) return first;
39
+ if (typeof err?.message === "string") return err.message;
40
+ return "Something went wrong completing your booking. Please try again.";
41
+ };
42
+
43
+ export default function BookingForm({ service, serviceName, slot, fields, onSuccess, onCancel }: Props) {
44
+ const [values, setValues] = useState<Record<string, string>>({});
45
+ const [status, setStatus] = useState<"idle" | "submitting">("idle");
41
46
  const [error, setError] = useState<string | null>(null);
42
- const [waitlisted, setWaitlisted] = useState(false);
43
47
 
44
- const isWaitlist = serviceType === "CLASS" && !!slot.isFull;
45
- // Party size only applies to CLASS (APPOINTMENT is 1-on-1).
46
- const maxParty = serviceType === "CLASS" ? Math.max(1, slot.bookableCapacity ?? slot.remainingCapacity ?? 1) : 1;
47
- const partyOptions = Array.from({ length: Math.min(maxParty, 8) }, (_, i) => i + 1);
48
+ const set = (target: string, v: string) =>
49
+ setValues((prev) => ({ ...prev, [target]: v }));
48
50
 
49
- const contactDetails = () => ({
50
- firstName: firstName.trim(),
51
- lastName: lastName.trim() || undefined,
52
- email: email.trim(),
53
- phone: phone.trim() || undefined,
51
+ const when = new Date(slot.localStartDate).toLocaleString([], {
52
+ weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit",
54
53
  });
55
54
 
56
- const buildSlot = () =>
57
- serviceType === "CLASS"
58
- // CLASS: serviceId + eventId only — Wix derives the rest from the event.
59
- ? { serviceId: slot.serviceId, eventId: slot.eventId }
60
- // APPOINTMENT: scheduleId + explicit start/end/timezone/location.
61
- : {
62
- serviceId: slot.serviceId,
63
- scheduleId: slot.scheduleId,
64
- startDate: slot.localStartDate,
65
- endDate: slot.localEndDate,
66
- timezone: slot.timezone,
67
- ...(slot.locationId
68
- ? { location: { _id: slot.locationId, locationType: slot.locationType as any } }
69
- : { location: { locationType: "OWNER_BUSINESS" as const } }),
70
- };
71
-
72
55
  const handleSubmit = async (e: React.FormEvent) => {
73
56
  e.preventDefault();
74
- setError(null);
75
- if (!firstName.trim() || !email.trim()) {
76
- setError("Please enter your first name and email.");
77
- return;
78
- }
79
- setSubmitting(true);
57
+ setStatus("submitting"); setError(null);
80
58
  try {
81
- const result = await wixClient.bookings.createBooking({
82
- totalParticipants: participants, // party size
83
- contactDetails: contactDetails(),
84
- selectedPaymentOption: "OFFLINE" as const,
85
- bookedEntity: { slot: buildSlot() },
86
- } as any);
87
-
88
- const status = result.booking?.status;
89
- const bookingId = result.booking?._id ?? "";
90
- const revision = result.booking?.revision ?? "";
91
-
92
- // Defensive: if createBooking ever returns a WAITING_LIST booking directly,
93
- // treat it as joined. In practice a full session THROWS
94
- // SESSION_CAPACITY_EXCEEDED (handled in catch → /api/waitlist) rather than
95
- // returning this status — both paths land the user on the waitlist.
96
- if (status === "WAITING_LIST") { setWaitlisted(true); setSubmitting(false); return; }
97
-
98
- // CREATED → confirm server-side to hold the seat.
99
- const confirmRes = await fetch("/api/confirm-booking", {
100
- method: "POST", headers: { "Content-Type": "application/json" },
101
- body: JSON.stringify({ bookingId, revision }),
59
+ const result = await book({
60
+ service,
61
+ slot,
62
+ formSubmission: values, // keyed by each field's `target`
63
+ timezone: slot.timezone,
102
64
  });
103
- const confirmJson = await confirmRes.json().catch(() => ({}));
104
- if (!confirmRes.ok || confirmJson.ok === false) {
105
- setError("This session just filled up — please pick another time.");
106
- setSubmitting(false); onCancel(); return;
65
+ if (result.type === BookResultType.CheckoutRequired) {
66
+ // Paid: hand the cart to the Wix-hosted checkout; it returns to /booking-confirmation.
67
+ // Carry service + start time on the return URL so the confirmation can show them
68
+ // (the in-memory selection is lost across the full-page redirect to Wix and back).
69
+ await navigateToCheckout(
70
+ result.cartId,
71
+ `${window.location.origin}/booking-confirmation?service=${encodeURIComponent(serviceName)}&startDate=${encodeURIComponent(slot.localStartDate)}`,
72
+ );
73
+ return; // redirect in progress
107
74
  }
108
- onSuccess(bookingId, slot.localStartDate);
109
- } catch (err: any) {
110
- console.error("[booking] createBooking/confirm failed:", err);
111
- // Full CLASS session → native waitlist. The error carries the v1 session id.
112
- const full = `${err?.message ?? ""} ${JSON.stringify(err?.details ?? {})} ${String(err)}`;
113
- if (/SESSION_CAPACITY_EXCEEDED/i.test(full) || /no available spots/i.test(full)) {
114
- const sessionId = full.match(/on session ([^\s"}]+)/)?.[1];
115
- if (sessionId) {
116
- try {
117
- const res = await fetch("/api/waitlist", {
118
- method: "POST", headers: { "Content-Type": "application/json" },
119
- body: JSON.stringify({ sessionId, totalParticipants: participants, contactDetails: contactDetails() }),
120
- });
121
- const j = await res.json().catch(() => ({}));
122
- if (res.ok && j.ok !== false) { setWaitlisted(true); setSubmitting(false); return; }
123
- } catch (e) { console.error("[waitlist] failed:", e); }
124
- }
125
- setError("This class is full and we couldn't add you to the waitlist — please try again later.");
126
- setSubmitting(false); return;
127
- }
128
- setError("This time slot was just taken. Please select another.");
129
- setSubmitting(false); onCancel();
75
+ onSuccess(result.orderId, slot.localStartDate); // free / pay-in-person
76
+ } catch (err) {
77
+ console.error("[booking] failed:", err);
78
+ setError(friendlyError(err));
79
+ setStatus("idle");
130
80
  }
131
81
  };
132
82
 
133
- if (waitlisted) {
134
- return (
135
- <div className="booking-form">
136
- <div className="booking-form-header">
137
- <h3 className="booking-form-title">You're on the waitlist</h3>
138
- <p className="booking-form-subtitle">{serviceName} · {slotDisplay(slot)}</p>
139
- </div>
140
- <p>We'll email <strong>{email.trim()}</strong> if a spot opens up.</p>
141
- <button type="button" className="booking-cancel" onClick={onCancel}>← Back to available times</button>
142
- </div>
143
- );
144
- }
145
-
146
83
  return (
147
84
  <form className="booking-form" onSubmit={handleSubmit}>
148
- <div className="booking-form-header">
149
- <h3 className="booking-form-title">{isWaitlist ? "Join the waitlist" : "Reserve your spot"}</h3>
150
- <p className="booking-form-subtitle">
151
- {serviceName} · {slotDisplay(slot)}{slot.instructorName ? ` · with ${slot.instructorName}` : ""}
152
- </p>
153
- {isWaitlist && <p className="booking-form-note">This session is full — join the waitlist and we'll notify you if a spot opens.</p>}
85
+ <div className="booking-form-summary">
86
+ <strong>{serviceName}</strong>
87
+ <span>{when}</span>
88
+ <button type="button" className="booking-form-change" onClick={onCancel}>Choose another time</button>
154
89
  </div>
155
90
 
156
- {error && <p className="booking-error">{error}</p>}
157
-
158
- <div className="booking-field-row">
159
- <div className="booking-field">
160
- <label className="booking-label booking-label--required" htmlFor="bf-first">First name</label>
161
- <input id="bf-first" className="booking-input" type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} required autoComplete="given-name" />
91
+ {fields.map((field) => (
92
+ <div key={field.target} className="booking-field">
93
+ <label className="booking-label">
94
+ {field.label}
95
+ {field.required && <span className="booking-required" aria-hidden="true"> *</span>}
96
+ </label>
97
+ {field.componentType === "DROPDOWN" && field.options ? (
98
+ <select
99
+ className="booking-input"
100
+ required={field.required}
101
+ value={values[field.target] ?? ""}
102
+ onChange={(e) => set(field.target, e.target.value)}
103
+ >
104
+ <option value="">Select an option</option>
105
+ {field.options.map((o) => (
106
+ <option key={o.value} value={o.value}>{o.label}</option>
107
+ ))}
108
+ </select>
109
+ ) : field.identifier === "TEXT_AREA" || field.target?.includes("message") ? (
110
+ <textarea
111
+ className="booking-input"
112
+ required={field.required}
113
+ rows={4}
114
+ value={values[field.target] ?? ""}
115
+ onChange={(e) => set(field.target, e.target.value)}
116
+ />
117
+ ) : (
118
+ <input
119
+ className="booking-input"
120
+ type={field.target === "email" ? "email" : field.componentType === "PHONE_INPUT" ? "tel" : "text"}
121
+ required={field.required}
122
+ value={values[field.target] ?? ""}
123
+ onChange={(e) => set(field.target, e.target.value)}
124
+ />
125
+ )}
162
126
  </div>
163
- <div className="booking-field">
164
- <label className="booking-label" htmlFor="bf-last">Last name</label>
165
- <input id="bf-last" className="booking-input" type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} autoComplete="family-name" />
166
- </div>
167
- </div>
127
+ ))}
168
128
 
169
- <div className="booking-field">
170
- <label className="booking-label booking-label--required" htmlFor="bf-email">Email</label>
171
- <input id="bf-email" className="booking-input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required autoComplete="email" />
172
- </div>
173
-
174
- <div className="booking-field">
175
- <label className="booking-label" htmlFor="bf-phone">Phone</label>
176
- <input id="bf-phone" className="booking-input" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} autoComplete="tel" />
177
- </div>
178
-
179
- {!isWaitlist && maxParty > 1 && (
180
- <div className="booking-field">
181
- <label className="booking-label" htmlFor="bf-party">Number of spots</label>
182
- <select id="bf-party" className="booking-input" value={participants} onChange={(e) => setParticipants(Number(e.target.value))}>
183
- {partyOptions.map((n) => <option key={n} value={n}>{n}</option>)}
184
- </select>
185
- </div>
186
- )}
129
+ {error && <p className="booking-error" role="alert">{error}</p>}
187
130
 
188
- <button type="submit" className="booking-submit" disabled={submitting}>
189
- {submitting ? (isWaitlist ? "Joining…" : "Confirming…") : isWaitlist ? "Join Waitlist" : "Confirm Booking"}
131
+ <button type="submit" className="booking-cta" disabled={status === "submitting"}>
132
+ {status === "submitting" ? "Booking…" : "Confirm booking"}
190
133
  </button>
191
- <button type="button" className="booking-cancel" onClick={onCancel} disabled={submitting}>← Back to available times</button>
192
134
  </form>
193
135
  );
194
136
  }
@@ -1,26 +1,30 @@
1
1
  import { useState } from "react";
2
2
  import AvailabilityCalendar from "./AvailabilityCalendar";
3
- import type { SelectedSlot } from "./AvailabilityCalendar";
4
- import BookingForm from "./BookingForm";
3
+ import BookingForm, { type BookingFormField } from "./BookingForm";
4
+ import type { SelectedSlot } from "./bookingDriver";
5
5
 
6
6
  // ServiceBookingFlow.tsx — client:only="react" coordinator island. Holds the
7
- // selected-slot state shared between AvailabilityCalendar and BookingForm,
8
- // transitions between them, and redirects to the confirmation page on success.
9
- // serviceType MUST be threaded through so the calendar picks the right time-slots
10
- // API and the form builds the right createBooking shape.
7
+ // selected-slot state shared between the calendar and the form, transitions
8
+ // between them, and redirects to the confirmation page on success. The SSR
9
+ // detail page passes the full `service` (the driver reads its payment/policy)
10
+ // and the booking-form `fields` (the @wix/forms schema) through to the form.
11
+ //
12
+ // This is the framework-agnostic flow shape (a step coordinator + shared
13
+ // selection state). On another framework, the same two steps + shared state can
14
+ // be route-driven instead — see references/bookings/FLOW.md.
11
15
 
12
16
  interface Props {
13
- serviceId: string;
17
+ service: any;
14
18
  serviceName: string;
15
19
  serviceType: "APPOINTMENT" | "CLASS";
20
+ fields: BookingFormField[];
16
21
  }
17
22
 
18
- export default function ServiceBookingFlow({ serviceId, serviceName, serviceType }: Props) {
23
+ export default function ServiceBookingFlow({ service, serviceName, serviceType, fields }: Props) {
19
24
  const [selectedSlot, setSelectedSlot] = useState<SelectedSlot | null>(null);
20
25
 
21
- const handleSuccess = (bookingId: string, startDate?: string) => {
26
+ const handleSuccess = (_orderId: string, startDate?: string) => {
22
27
  const params = new URLSearchParams();
23
- if (bookingId) params.set("bookingId", bookingId);
24
28
  if (startDate) params.set("startDate", startDate);
25
29
  if (serviceName) params.set("service", serviceName);
26
30
  window.location.href = `/booking-confirmation?${params.toString()}`;
@@ -29,7 +33,7 @@ export default function ServiceBookingFlow({ serviceId, serviceName, serviceType
29
33
  if (!selectedSlot) {
30
34
  return (
31
35
  <AvailabilityCalendar
32
- serviceId={serviceId}
36
+ serviceId={service._id}
33
37
  serviceName={serviceName}
34
38
  serviceType={serviceType}
35
39
  onSlotSelected={setSelectedSlot}
@@ -39,10 +43,10 @@ export default function ServiceBookingFlow({ serviceId, serviceName, serviceType
39
43
 
40
44
  return (
41
45
  <BookingForm
42
- serviceId={serviceId}
46
+ service={service}
43
47
  serviceName={serviceName}
44
- serviceType={serviceType}
45
48
  slot={selectedSlot}
49
+ fields={fields}
46
50
  onSuccess={handleSuccess}
47
51
  onCancel={() => setSelectedSlot(null)}
48
52
  />
@@ -1,50 +1,16 @@
1
1
  ---
2
2
  // src/pages/booking-confirmation.astro — post-booking confirmation.
3
3
  //
4
- // Shows the actual booking status (so the user can see it was actually
5
- // confirmed/approved — not just an optimistic "you're booked") and lets them
6
- // cancel inline. It re-fetches the booking with the anonymous action token: the
7
- // server elevates (app identity) to mint a per-booking token, then reads it with
8
- // the no-auth `bookingsGetBookingAnonymously`. Falls back to the redirect params
9
- // if the lookup fails. Cancel is handled by the same <ManageBooking> island used
10
- // on /manage-booking.
4
+ // Renders from the redirect query params (service name + start date) that the
5
+ // booking step passes through. The Wix-hosted checkout returns here after a paid
6
+ // booking; a free / pay-in-person booking lands here directly. Do not re-fetch a
7
+ // stranger's booking from this public page — show what the flow passed in.
11
8
  import Layout from "../layouts/Layout.astro";
12
- import ManageBooking from "../components/ManageBooking";
13
- import { auth } from "@wix/essentials";
14
- import { bookings } from "@wix/bookings";
15
9
 
16
- const bookingId = Astro.url.searchParams.get("bookingId");
10
+ const serviceName = Astro.url.searchParams.get("service") ?? "your service";
17
11
  const startDateParam = Astro.url.searchParams.get("startDate");
18
- const serviceParam = Astro.url.searchParams.get("service") ?? "your service";
19
-
20
- let token: string | null = null;
21
- let booking: any = null;
22
- let canCancel = false;
23
- if (bookingId) {
24
- try {
25
- const tokenRes = await auth.elevate(bookings.getAnonymousActionToken)(bookingId);
26
- token = tokenRes.token ?? null;
27
- if (token) {
28
- const res = await bookings.bookingsGetBookingAnonymously(token);
29
- booking = res.booking ?? null;
30
- canCancel = !!res.allowedAnonymousActions?.cancel;
31
- }
32
- } catch (err) {
33
- console.error("[booking-confirmation] status lookup failed:", err);
34
- }
35
- }
36
-
37
- const status = (booking?.status as string | undefined) ?? null; // CONFIRMED | PENDING | …
38
- const isPending = status === "PENDING" || status === "PENDING_APPROVAL";
39
- const serviceName = booking?.bookedEntity?.title ?? serviceParam;
40
- const startDate =
41
- (booking?.startDate as string | undefined) ??
42
- booking?.bookedEntity?.slot?.startDate ??
43
- startDateParam ??
44
- null;
45
- const revision = (booking?.revision as string | undefined) ?? "";
46
- const formattedDate = startDate
47
- ? new Date(startDate).toLocaleString("en-US", {
12
+ const formattedDate = startDateParam
13
+ ? new Date(startDateParam).toLocaleString("en-US", {
48
14
  weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit",
49
15
  })
50
16
  : null;
@@ -52,59 +18,31 @@ const formattedDate = startDate
52
18
 
53
19
  <!-- `max-w-prose` (built-in literal = 65ch), not a bare max-w-<size> that could
54
20
  collapse to the spacing scale. -->
55
- <Layout title={isPending ? "Booking received" : "Booking confirmed"}>
21
+ <Layout title="Booking confirmed">
56
22
  <section class="max-w-prose mx-auto px-lg py-4xl text-center">
57
23
  <div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-xl"
58
- style="background-color: var(--color-ink);" aria-hidden="true">
59
- <span class="text-2xl" style="color: var(--color-paper);">{isPending ? "⏳" : "✓"}</span>
24
+ style="background-color: var(--color-accent);" aria-hidden="true">
25
+ <span class="text-2xl" style="color: var(--color-paper);">✓</span>
60
26
  </div>
61
27
  <hr class="editorial-rule w-16 mx-auto mb-lg" />
62
- <h1 class="font-display text-4xl md:text-5xl leading-tight mb-md">
63
- {isPending ? "Booking received" : "Thank you for booking"}
64
- </h1>
28
+ <h1 class="font-display text-4xl md:text-5xl leading-tight mb-md">Thank you for booking</h1>
65
29
 
66
30
  {
67
- isPending ? (
68
- <p class="text-lg text-mute leading-relaxed mb-md">
69
- Your request for <strong class="text-ink">{serviceName}</strong>
70
- {formattedDate ? <> on <strong class="text-ink">{formattedDate}</strong></> : null} is
71
- <strong class="text-ink"> awaiting confirmation</strong>. Return here anytime to check status or cancel below.
72
- </p>
73
- ) : formattedDate ? (
31
+ formattedDate ? (
74
32
  <p class="text-lg text-mute leading-relaxed mb-md">
75
33
  Your spot in <strong class="text-ink">{serviceName}</strong> is <strong class="text-ink">confirmed</strong> for{" "}
76
- <strong class="text-ink">{formattedDate}</strong>.
34
+ <strong class="text-ink">{formattedDate}</strong>. We've sent the details to your email.
77
35
  </p>
78
- ) : bookingId ? (
36
+ ) : (
79
37
  <p class="text-lg text-mute leading-relaxed mb-md">
80
- Your spot in <strong class="text-ink">{serviceName}</strong> is <strong class="text-ink">confirmed</strong>.
38
+ Your booking for <strong class="text-ink">{serviceName}</strong> is <strong class="text-ink">confirmed</strong>. We've sent the details to your email.
81
39
  </p>
82
- ) : (
83
- <p class="text-lg text-mute leading-relaxed mb-md">Looking to book? Browse our services.</p>
84
- )
85
- }
86
-
87
- {/* Inline cancel via ManageBooking (same anonymous-token flow as /manage-booking). */}
88
- {
89
- token && bookingId && (
90
- <div class="mb-xl flex flex-col items-center gap-md">
91
- <ManageBooking
92
- token={token}
93
- revision={revision}
94
- serviceName={serviceName}
95
- startDate={startDate}
96
- status={status}
97
- canCancel={canCancel}
98
- showSummary={false}
99
- client:only="react"
100
- />
101
- </div>
102
40
  )
103
41
  }
104
42
 
105
43
  <div class="flex flex-col gap-md items-center">
106
44
  <a href="/services" class="inline-block font-display text-base px-xl py-md rounded-sm"
107
- style="background-color: var(--color-ink); color: var(--color-paper);">Browse more services</a>
45
+ style="background-color: var(--color-accent); color: var(--color-paper);">Browse more services</a>
108
46
  <a href="/" class="text-sm text-mute hover:text-ink">Return home</a>
109
47
  </div>
110
48
  </section>