@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,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,30 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import AvailabilityCalendar from "./AvailabilityCalendar";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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
|
+
// 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
|
-
|
|
17
|
+
service: any;
|
|
14
18
|
serviceName: string;
|
|
15
19
|
serviceType: "APPOINTMENT" | "CLASS";
|
|
20
|
+
fields: BookingFormField[];
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
export default function ServiceBookingFlow({
|
|
23
|
+
export default function ServiceBookingFlow({ service, serviceName, serviceType, fields }: Props) {
|
|
19
24
|
const [selectedSlot, setSelectedSlot] = useState<SelectedSlot | null>(null);
|
|
20
25
|
|
|
21
|
-
const handleSuccess = (
|
|
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={
|
|
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
|
-
|
|
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
|
-
//
|
|
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=
|
|
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-
|
|
59
|
-
<span class="text-2xl" style="color: var(--color-paper);"
|
|
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
|
-
|
|
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
|
-
) :
|
|
36
|
+
) : (
|
|
79
37
|
<p class="text-lg text-mute leading-relaxed mb-md">
|
|
80
|
-
Your
|
|
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-
|
|
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>
|