@wix/agent-skills 1.8.0 → 1.10.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/DESIGN_SYSTEM.md +6 -16
- 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 +77 -428
- package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +2 -2
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +7 -10
- package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +2 -2
- package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +3 -3
- package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +2 -2
- package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +7 -7
- package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +4 -4
- package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +2 -2
- package/skills/wix-headless/references/astro/templates/Footer.astro +4 -4
- package/skills/wix-headless/references/astro/templates/Navigation.astro +4 -4
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +298 -142
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +35 -14
- package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +11 -2
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +23 -85
- package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +209 -198
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +114 -19
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +131 -15
- package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +3 -3
- package/skills/wix-headless/references/astro/templates/ecom/cart.astro +3 -3
- package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +17 -17
- package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +3 -3
- package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +4 -4
- package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +19 -18
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +3 -3
- package/skills/wix-headless/references/astro/templates/global.css +17 -15
- package/skills/wix-headless/references/astro/templates/index.astro +4 -4
- package/skills/wix-headless/references/astro/templates/stores/components-stores.css +31 -31
- package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +4 -4
- package/skills/wix-headless/references/bookings/FLOW.md +246 -0
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +65 -76
- package/skills/wix-headless/references/bookings/SERVICES_DATA.md +60 -0
- package/skills/wix-headless/references/custom/bookings/WIRING.md +169 -60
- package/skills/wix-headless/references/shared/DESIGN_MD.md +2 -3
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +4 -4
- package/skills/wix-headless/references/shared/STYLING.md +15 -18
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +2 -2
- package/skills/wix-headless/references/verticals/bookings.md +9 -9
- package/skills/wix-headless/scripts/compose.mjs +4 -19
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -3
- 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 +254 -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 +233 -0
- package/skills/wix-manage/references/bookings/create-course-service.md +237 -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,194 +1,136 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
|
|
26
|
+
service: any; // SSR-fetched service — bookingDriver reads payment + bookingPolicy
|
|
15
27
|
serviceName: string;
|
|
16
|
-
serviceType: "APPOINTMENT" | "CLASS";
|
|
17
28
|
slot: SelectedSlot;
|
|
18
|
-
|
|
29
|
+
fields: BookingFormField[];
|
|
30
|
+
onSuccess: (id: string, startDate?: string) => void;
|
|
19
31
|
onCancel: () => void;
|
|
20
32
|
}
|
|
21
33
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
45
|
-
|
|
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
|
|
50
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
109
|
-
} catch (err
|
|
110
|
-
console.error("[booking]
|
|
111
|
-
|
|
112
|
-
|
|
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-
|
|
149
|
-
<
|
|
150
|
-
<
|
|
151
|
-
|
|
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
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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-
|
|
189
|
-
{
|
|
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,44 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import AvailabilityCalendar from "./AvailabilityCalendar";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import AvailabilityCalendar, { type StaffMemberOption, type LocationOption } from "./AvailabilityCalendar";
|
|
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
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
+
// the booking-form `fields` (the @wix/forms schema), the service's `staffMembers`
|
|
11
|
+
// (for the optional staff picker), the service's business `locations` + the
|
|
12
|
+
// catalog-chosen `locationId` (so the calendar scopes availability to one
|
|
13
|
+
// location — see AvailabilityCalendar) through to the calendar.
|
|
14
|
+
//
|
|
15
|
+
// This is the framework-agnostic flow shape (a step coordinator + shared
|
|
16
|
+
// selection state). On another framework, the same two steps + shared state can
|
|
17
|
+
// be route-driven instead — see references/bookings/FLOW.md.
|
|
11
18
|
|
|
12
19
|
interface Props {
|
|
13
|
-
|
|
20
|
+
service: any;
|
|
14
21
|
serviceName: string;
|
|
15
22
|
serviceType: "APPOINTMENT" | "CLASS";
|
|
23
|
+
fields: BookingFormField[];
|
|
24
|
+
staffMembers?: StaffMemberOption[];
|
|
25
|
+
locations?: LocationOption[];
|
|
26
|
+
locationId?: string;
|
|
16
27
|
}
|
|
17
28
|
|
|
18
|
-
export default function ServiceBookingFlow({
|
|
29
|
+
export default function ServiceBookingFlow({
|
|
30
|
+
service,
|
|
31
|
+
serviceName,
|
|
32
|
+
serviceType,
|
|
33
|
+
fields,
|
|
34
|
+
staffMembers,
|
|
35
|
+
locations,
|
|
36
|
+
locationId,
|
|
37
|
+
}: Props) {
|
|
19
38
|
const [selectedSlot, setSelectedSlot] = useState<SelectedSlot | null>(null);
|
|
20
39
|
|
|
21
|
-
const handleSuccess = (
|
|
40
|
+
const handleSuccess = (_orderId: string, startDate?: string) => {
|
|
22
41
|
const params = new URLSearchParams();
|
|
23
|
-
if (bookingId) params.set("bookingId", bookingId);
|
|
24
42
|
if (startDate) params.set("startDate", startDate);
|
|
25
43
|
if (serviceName) params.set("service", serviceName);
|
|
26
44
|
window.location.href = `/booking-confirmation?${params.toString()}`;
|
|
@@ -29,9 +47,12 @@ export default function ServiceBookingFlow({ serviceId, serviceName, serviceType
|
|
|
29
47
|
if (!selectedSlot) {
|
|
30
48
|
return (
|
|
31
49
|
<AvailabilityCalendar
|
|
32
|
-
serviceId={
|
|
50
|
+
serviceId={service._id}
|
|
33
51
|
serviceName={serviceName}
|
|
34
52
|
serviceType={serviceType}
|
|
53
|
+
staffMembers={staffMembers}
|
|
54
|
+
locations={locations}
|
|
55
|
+
locationId={locationId}
|
|
35
56
|
onSlotSelected={setSelectedSlot}
|
|
36
57
|
/>
|
|
37
58
|
);
|
|
@@ -39,10 +60,10 @@ export default function ServiceBookingFlow({ serviceId, serviceName, serviceType
|
|
|
39
60
|
|
|
40
61
|
return (
|
|
41
62
|
<BookingForm
|
|
42
|
-
|
|
63
|
+
service={service}
|
|
43
64
|
serviceName={serviceName}
|
|
44
|
-
serviceType={serviceType}
|
|
45
65
|
slot={selectedSlot}
|
|
66
|
+
fields={fields}
|
|
46
67
|
onSuccess={handleSuccess}
|
|
47
68
|
onCancel={() => setSelectedSlot(null)}
|
|
48
69
|
/>
|
|
@@ -14,12 +14,21 @@ interface Props {
|
|
|
14
14
|
currency?: string;
|
|
15
15
|
imageUrl?: string;
|
|
16
16
|
type: "APPOINTMENT" | "CLASS";
|
|
17
|
+
locationId?: string; // when a location is selected on the catalog, carry it to the detail page
|
|
18
|
+
locationType?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const { slug, name, tagLine, durationMinutes, price, imageUrl } = Astro.props;
|
|
21
|
+
const { slug, name, tagLine, durationMinutes, price, imageUrl, locationId, locationType } = Astro.props;
|
|
22
|
+
|
|
23
|
+
// Carry the selected location into the detail page so availability is scoped to it.
|
|
24
|
+
const q = new URLSearchParams();
|
|
25
|
+
if (locationId) q.set("locationId", locationId);
|
|
26
|
+
if (locationType) q.set("locationType", locationType);
|
|
27
|
+
const qs = q.toString();
|
|
28
|
+
const href = qs ? `/services/${slug}?${qs}` : `/services/${slug}`;
|
|
20
29
|
---
|
|
21
30
|
|
|
22
|
-
<a href={
|
|
31
|
+
<a href={href} class="service-card">
|
|
23
32
|
{
|
|
24
33
|
imageUrl ? (
|
|
25
34
|
<div class="service-card-image">
|
|
@@ -1,50 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
// src/pages/booking-confirmation.astro — post-booking confirmation.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
10
|
+
const serviceName = Astro.url.searchParams.get("service") ?? "your service";
|
|
17
11
|
const startDateParam = Astro.url.searchParams.get("startDate");
|
|
18
|
-
const
|
|
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=
|
|
56
|
-
<section class="max-w-prose mx-auto px-
|
|
57
|
-
<div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-
|
|
58
|
-
style="background-color: var(--color-
|
|
59
|
-
<span class="text-2xl" style="color: var(--color-paper);"
|
|
21
|
+
<Layout title="Booking confirmed">
|
|
22
|
+
<section class="max-w-prose mx-auto px-6 py-24 text-center">
|
|
23
|
+
<div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-8"
|
|
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
|
-
<hr class="editorial-rule w-16 mx-auto mb-
|
|
62
|
-
<h1 class="font-display text-4xl md:text-5xl leading-tight mb-
|
|
63
|
-
{isPending ? "Booking received" : "Thank you for booking"}
|
|
64
|
-
</h1>
|
|
27
|
+
<hr class="editorial-rule w-16 mx-auto mb-6" />
|
|
28
|
+
<h1 class="font-display text-4xl md:text-5xl leading-tight mb-4">Thank you for booking</h1>
|
|
65
29
|
|
|
66
30
|
{
|
|
67
|
-
|
|
68
|
-
<p class="text-lg text-mute leading-relaxed mb-
|
|
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 ? (
|
|
74
|
-
<p class="text-lg text-mute leading-relaxed mb-md">
|
|
31
|
+
formattedDate ? (
|
|
32
|
+
<p class="text-lg text-mute leading-relaxed mb-4">
|
|
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>.
|
|
77
|
-
</p>
|
|
78
|
-
) : bookingId ? (
|
|
79
|
-
<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>.
|
|
34
|
+
<strong class="text-ink">{formattedDate}</strong>. We've sent the details to your email.
|
|
81
35
|
</p>
|
|
82
36
|
) : (
|
|
83
|
-
<p class="text-lg text-mute leading-relaxed mb-
|
|
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>
|
|
37
|
+
<p class="text-lg text-mute leading-relaxed mb-4">
|
|
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.
|
|
39
|
+
</p>
|
|
102
40
|
)
|
|
103
41
|
}
|
|
104
42
|
|
|
105
|
-
<div class="flex flex-col gap-
|
|
106
|
-
<a href="/services" class="inline-block font-display text-base px-
|
|
107
|
-
style="background-color: var(--color-
|
|
43
|
+
<div class="flex flex-col gap-4 items-center">
|
|
44
|
+
<a href="/services" class="inline-block font-display text-base px-8 py-4 rounded-sm"
|
|
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>
|