@riverbankcms/sdk 0.2.0 → 0.2.1
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/dist/cli/index.js +4840 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/client/bookings.d.mts +82 -2
- package/dist/client/bookings.d.ts +82 -2
- package/dist/client/bookings.js +1623 -3
- package/dist/client/bookings.js.map +1 -1
- package/dist/client/bookings.mjs +1610 -5
- package/dist/client/bookings.mjs.map +1 -1
- package/dist/client/client.d.mts +8 -5
- package/dist/client/client.d.ts +8 -5
- package/dist/client/client.js +16856 -322
- package/dist/client/client.js.map +1 -1
- package/dist/client/client.mjs +16838 -307
- package/dist/client/client.mjs.map +1 -1
- package/dist/client/hooks.d.mts +10 -7
- package/dist/client/hooks.d.ts +10 -7
- package/dist/client/hooks.js +5074 -4
- package/dist/client/hooks.js.map +1 -1
- package/dist/client/hooks.mjs +5074 -4
- package/dist/client/hooks.mjs.map +1 -1
- package/dist/client/rendering/client.d.mts +7 -1
- package/dist/client/rendering/client.d.ts +7 -1
- package/dist/client/rendering/client.js +17388 -2
- package/dist/client/rendering/client.js.map +1 -1
- package/dist/client/rendering/client.mjs +17382 -2
- package/dist/client/rendering/client.mjs.map +1 -1
- package/dist/client/resolver-BhueZVxZ.d.mts +61 -0
- package/dist/client/resolver-BhueZVxZ.d.ts +61 -0
- package/dist/client/usePage-BBcFCxOU.d.ts +6297 -0
- package/dist/client/usePage-BydHcMYB.d.mts +6297 -0
- package/dist/server/Layout-CLg8oH_S.d.ts +44 -0
- package/dist/server/Layout-DK_9OOgb.d.mts +44 -0
- package/dist/server/chunk-3J46ILMJ.mjs +2111 -0
- package/dist/server/chunk-3J46ILMJ.mjs.map +1 -0
- package/dist/server/{chunk-JB4LIEFS.js → chunk-5R4NMVXA.js} +15 -8
- package/dist/server/chunk-5R4NMVXA.js.map +1 -0
- package/dist/server/{chunk-ADREPXFU.js → chunk-62ZJI564.js} +3 -3
- package/dist/server/{chunk-ADREPXFU.js.map → chunk-62ZJI564.js.map} +1 -1
- package/dist/server/chunk-7DS4Q3GA.mjs +333 -0
- package/dist/server/chunk-7DS4Q3GA.mjs.map +1 -0
- package/dist/server/chunk-BJTO5JO5.mjs +11 -0
- package/dist/server/{chunk-4Z5FBFRL.mjs → chunk-BPKYRPCQ.mjs} +7 -3
- package/dist/server/{chunk-4Z5FBFRL.mjs.map → chunk-BPKYRPCQ.mjs.map} +1 -1
- package/dist/server/chunk-DGUM43GV.js +11 -0
- package/dist/server/chunk-DGUM43GV.js.map +1 -0
- package/dist/server/chunk-EGTDJ4PL.js +5461 -0
- package/dist/server/chunk-EGTDJ4PL.js.map +1 -0
- package/dist/server/chunk-FK64TZBT.mjs +831 -0
- package/dist/server/chunk-FK64TZBT.mjs.map +1 -0
- package/dist/server/chunk-GKYNDDJS.js +2111 -0
- package/dist/server/chunk-GKYNDDJS.js.map +1 -0
- package/dist/server/chunk-HOY77YBF.js +333 -0
- package/dist/server/chunk-HOY77YBF.js.map +1 -0
- package/dist/server/chunk-INWKF3IC.js +831 -0
- package/dist/server/chunk-INWKF3IC.js.map +1 -0
- package/dist/server/{chunk-2RW5HAQQ.mjs → chunk-JTAERCX2.mjs} +2 -2
- package/dist/server/chunk-O5DC7MYW.mjs +9606 -0
- package/dist/server/chunk-O5DC7MYW.mjs.map +1 -0
- package/dist/server/{chunk-PEAXKTDU.mjs → chunk-OP2GHK27.mjs} +2 -2
- package/dist/server/{chunk-WKG57P2H.mjs → chunk-PN3CHDVX.mjs} +10 -3
- package/dist/server/{chunk-WKG57P2H.mjs.map → chunk-PN3CHDVX.mjs.map} +1 -1
- package/dist/server/chunk-SF63XAX7.js +9606 -0
- package/dist/server/chunk-SF63XAX7.js.map +1 -0
- package/dist/server/{chunk-F472SMKX.js → chunk-TO7FD6TQ.js} +4 -4
- package/dist/server/{chunk-F472SMKX.js.map → chunk-TO7FD6TQ.js.map} +1 -1
- package/dist/server/chunk-USQF2XTU.mjs +5461 -0
- package/dist/server/chunk-USQF2XTU.mjs.map +1 -0
- package/dist/server/{chunk-SW7LE4M3.js → chunk-XLVL5WPH.js} +12 -8
- package/dist/server/chunk-XLVL5WPH.js.map +1 -0
- package/dist/server/components-BzdA6NAc.d.mts +305 -0
- package/dist/server/components-DhIcstww.d.ts +305 -0
- package/dist/server/components.d.mts +13 -49
- package/dist/server/components.d.ts +13 -49
- package/dist/server/components.js +7 -4
- package/dist/server/components.js.map +1 -1
- package/dist/server/components.mjs +9 -6
- package/dist/server/components.mjs.map +1 -1
- package/dist/server/config-validation.d.mts +2 -2
- package/dist/server/config-validation.d.ts +2 -2
- package/dist/server/config-validation.js +6 -3
- package/dist/server/config-validation.js.map +1 -1
- package/dist/server/config-validation.mjs +5 -2
- package/dist/server/config.d.mts +3 -3
- package/dist/server/config.d.ts +3 -3
- package/dist/server/config.js +6 -3
- package/dist/server/config.js.map +1 -1
- package/dist/server/config.mjs +5 -2
- package/dist/server/config.mjs.map +1 -1
- package/dist/server/data.d.mts +9 -8
- package/dist/server/data.d.ts +9 -8
- package/dist/server/data.js +4 -2
- package/dist/server/data.js.map +1 -1
- package/dist/server/data.mjs +3 -1
- package/dist/server/{index-C6M0Wfjq.d.ts → index-BB28KAui.d.ts} +1 -1
- package/dist/server/{index-B0yI_V6Z.d.mts → index-C_FVup_o.d.mts} +1 -1
- package/dist/server/index.d.mts +1554 -5
- package/dist/server/index.d.ts +1554 -5
- package/dist/server/index.js +4 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +4 -4
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/{loadContent-CJcbYF3J.d.ts → loadContent-AQOBf_gP.d.ts} +4 -4
- package/dist/server/{loadContent-zhlL4YSE.d.mts → loadContent-DBmprsB4.d.mts} +4 -4
- package/dist/server/loadPage-3ECPF426.js +11 -0
- package/dist/server/loadPage-3ECPF426.js.map +1 -0
- package/dist/server/{loadPage-CCf15nt8.d.mts → loadPage-BMg8PJxJ.d.ts} +146 -5
- package/dist/server/loadPage-LW273NYO.mjs +11 -0
- package/dist/server/loadPage-LW273NYO.mjs.map +1 -0
- package/dist/server/{loadPage-BYmVMk0V.d.ts → loadPage-pg4HimlK.d.mts} +146 -5
- package/dist/server/metadata.d.mts +9 -6
- package/dist/server/metadata.d.ts +9 -6
- package/dist/server/metadata.js +3 -1
- package/dist/server/metadata.js.map +1 -1
- package/dist/server/metadata.mjs +2 -0
- package/dist/server/metadata.mjs.map +1 -1
- package/dist/server/rendering/server.d.mts +9 -7
- package/dist/server/rendering/server.d.ts +9 -7
- package/dist/server/rendering/server.js +7 -4
- package/dist/server/rendering/server.js.map +1 -1
- package/dist/server/rendering/server.mjs +6 -3
- package/dist/server/rendering.d.mts +172 -9
- package/dist/server/rendering.d.ts +172 -9
- package/dist/server/rendering.js +12 -9
- package/dist/server/rendering.js.map +1 -1
- package/dist/server/rendering.mjs +14 -11
- package/dist/server/rendering.mjs.map +1 -1
- package/dist/server/routing.d.mts +9 -6
- package/dist/server/routing.d.ts +9 -6
- package/dist/server/routing.js +4 -2
- package/dist/server/routing.js.map +1 -1
- package/dist/server/routing.mjs +3 -1
- package/dist/server/routing.mjs.map +1 -1
- package/dist/server/schema-Bpy9N5ZI.d.mts +1870 -0
- package/dist/server/schema-Bpy9N5ZI.d.ts +1870 -0
- package/dist/server/server.d.mts +11 -8
- package/dist/server/server.d.ts +11 -8
- package/dist/server/server.js +7 -5
- package/dist/server/server.js.map +1 -1
- package/dist/server/server.mjs +6 -4
- package/dist/server/theme-bridge.js +13 -10
- package/dist/server/theme-bridge.js.map +1 -1
- package/dist/server/theme-bridge.mjs +10 -7
- package/dist/server/theme-bridge.mjs.map +1 -1
- package/dist/server/theme.js +3 -1
- package/dist/server/theme.js.map +1 -1
- package/dist/server/theme.mjs +2 -0
- package/dist/server/theme.mjs.map +1 -1
- package/dist/server/{types-BCeqWtI2.d.ts → types--u4GLCAY.d.ts} +1 -1
- package/dist/server/types-BprgZt-t.d.ts +4149 -0
- package/dist/server/types-C0G9IxWO.d.mts +4149 -0
- package/dist/server/{types-Bbo01M7P.d.mts → types-_nDnPHpv.d.mts} +27 -1
- package/dist/server/{types-Bbo01M7P.d.ts → types-_nDnPHpv.d.ts} +27 -1
- package/dist/server/{types-BCeqWtI2.d.mts → types-_zWJTgv0.d.mts} +1 -1
- package/package.json +6 -6
- package/dist/server/chunk-3KKZVGH4.mjs +0 -179
- package/dist/server/chunk-3KKZVGH4.mjs.map +0 -1
- package/dist/server/chunk-4Z3GPTCS.js +0 -179
- package/dist/server/chunk-4Z3GPTCS.js.map +0 -1
- package/dist/server/chunk-JB4LIEFS.js.map +0 -1
- package/dist/server/chunk-QQ6U4QX6.js +0 -120
- package/dist/server/chunk-QQ6U4QX6.js.map +0 -1
- package/dist/server/chunk-R5YGLRUG.mjs +0 -122
- package/dist/server/chunk-R5YGLRUG.mjs.map +0 -1
- package/dist/server/chunk-SW7LE4M3.js.map +0 -1
- package/dist/server/chunk-W3K7LVPS.mjs +0 -120
- package/dist/server/chunk-W3K7LVPS.mjs.map +0 -1
- package/dist/server/chunk-YHEZMVTS.js +0 -122
- package/dist/server/chunk-YHEZMVTS.js.map +0 -1
- package/dist/server/loadPage-DVH3DW6E.js +0 -9
- package/dist/server/loadPage-DVH3DW6E.js.map +0 -1
- package/dist/server/loadPage-PHQZ6XQZ.mjs +0 -9
- package/dist/server/types-C6gmRHLe.d.mts +0 -150
- package/dist/server/types-C6gmRHLe.d.ts +0 -150
- /package/dist/server/{loadPage-PHQZ6XQZ.mjs.map → chunk-BJTO5JO5.mjs.map} +0 -0
- /package/dist/server/{chunk-2RW5HAQQ.mjs.map → chunk-JTAERCX2.mjs.map} +0 -0
- /package/dist/server/{chunk-PEAXKTDU.mjs.map → chunk-OP2GHK27.mjs.map} +0 -0
package/dist/client/bookings.mjs
CHANGED
|
@@ -1,9 +1,1614 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
// src/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
// ../blocks/src/system/runtime/utils/api-url.ts
|
|
3
|
+
function getCmsApiUrl() {
|
|
4
|
+
const builderApiUrl = process.env.NEXT_PUBLIC_BUILDER_API_URL;
|
|
5
|
+
if (builderApiUrl) {
|
|
6
|
+
return builderApiUrl.replace(/\/$/, "");
|
|
7
|
+
}
|
|
8
|
+
const dashboardUrl = process.env.NEXT_PUBLIC_DASHBOARD_URL;
|
|
9
|
+
if (dashboardUrl) {
|
|
10
|
+
const base = dashboardUrl.replace(/\/$/, "");
|
|
11
|
+
return `${base}/api`;
|
|
12
|
+
}
|
|
13
|
+
const legacyApiUrl = process.env.NEXT_PUBLIC_CMS_API_URL;
|
|
14
|
+
if (legacyApiUrl) {
|
|
15
|
+
return legacyApiUrl.replace(/\/$/, "");
|
|
16
|
+
}
|
|
17
|
+
throw new Error(
|
|
18
|
+
"No API URL configured. Set NEXT_PUBLIC_BUILDER_API_URL (SDK sites) or NEXT_PUBLIC_DASHBOARD_URL (frontend app)."
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../blocks/src/system/runtime/nodes/booking-form.client.tsx
|
|
23
|
+
import React5, { useState as useState7 } from "react";
|
|
24
|
+
|
|
25
|
+
// ../blocks/src/system/runtime/components/multi-step/MultiStepForm.tsx
|
|
26
|
+
import { cloneElement, isValidElement } from "react";
|
|
27
|
+
|
|
28
|
+
// ../blocks/src/system/runtime/components/multi-step/MultiStepContext.tsx
|
|
29
|
+
import { createContext, useContext } from "react";
|
|
30
|
+
var MultiStepContext = createContext(null);
|
|
31
|
+
function useMultiStepContext() {
|
|
32
|
+
const context = useContext(MultiStepContext);
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"useMultiStepContext must be used within a MultiStepForm component. Make sure your component is wrapped in <MultiStepForm>."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
function useMultiStepData() {
|
|
41
|
+
const { data, updateData } = useMultiStepContext();
|
|
42
|
+
return { data, updateData };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ../blocks/src/system/runtime/components/multi-step/useMultiStep.ts
|
|
46
|
+
import { useState, useCallback, useMemo, useEffect } from "react";
|
|
47
|
+
function useMultiStep({
|
|
48
|
+
steps,
|
|
49
|
+
initialData = {},
|
|
50
|
+
onStepChange,
|
|
51
|
+
onDataChange,
|
|
52
|
+
onComplete,
|
|
53
|
+
persistToUrl = false
|
|
54
|
+
}) {
|
|
55
|
+
const [data, setData] = useState(initialData);
|
|
56
|
+
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
|
57
|
+
const [errors, setErrors] = useState({});
|
|
58
|
+
const [isValidating, setIsValidating] = useState(false);
|
|
59
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
60
|
+
const visibleSteps = useMemo(() => {
|
|
61
|
+
return steps.filter((step) => {
|
|
62
|
+
if (!step.condition) return true;
|
|
63
|
+
return step.condition(data);
|
|
64
|
+
});
|
|
65
|
+
}, [steps, data]);
|
|
66
|
+
const currentStep = visibleSteps[currentStepIndex];
|
|
67
|
+
const currentStepId = currentStep?.id || "";
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (persistToUrl && currentStep) {
|
|
70
|
+
const url = new URL(window.location.href);
|
|
71
|
+
url.searchParams.set("step", currentStep.id);
|
|
72
|
+
window.history.replaceState({}, "", url.toString());
|
|
73
|
+
}
|
|
74
|
+
}, [currentStepIndex, currentStep, persistToUrl]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (persistToUrl) {
|
|
77
|
+
const url = new URL(window.location.href);
|
|
78
|
+
const stepId = url.searchParams.get("step");
|
|
79
|
+
if (stepId) {
|
|
80
|
+
const stepIndex = visibleSteps.findIndex((s) => s.id === stepId);
|
|
81
|
+
if (stepIndex !== -1) {
|
|
82
|
+
setCurrentStepIndex(stepIndex);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, []);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (onStepChange && currentStep) {
|
|
89
|
+
onStepChange(currentStep.id, currentStepIndex);
|
|
90
|
+
}
|
|
91
|
+
}, [currentStepIndex, currentStep, onStepChange]);
|
|
92
|
+
const updateData = useCallback(
|
|
93
|
+
(updates) => {
|
|
94
|
+
setData((prev) => {
|
|
95
|
+
const newData = { ...prev, ...updates };
|
|
96
|
+
onDataChange?.(newData);
|
|
97
|
+
return newData;
|
|
98
|
+
});
|
|
99
|
+
setErrors({});
|
|
100
|
+
},
|
|
101
|
+
[onDataChange]
|
|
102
|
+
);
|
|
103
|
+
const validateCurrentStep = useCallback(async () => {
|
|
104
|
+
if (!currentStep?.validate) return true;
|
|
105
|
+
setIsValidating(true);
|
|
106
|
+
setErrors({});
|
|
107
|
+
try {
|
|
108
|
+
const result = await currentStep.validate(data);
|
|
109
|
+
if (result.valid) {
|
|
110
|
+
return true;
|
|
111
|
+
} else {
|
|
112
|
+
setErrors(result.errors);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Step validation error:", error);
|
|
117
|
+
setErrors({ _form: "Validation failed. Please try again." });
|
|
118
|
+
return false;
|
|
119
|
+
} finally {
|
|
120
|
+
setIsValidating(false);
|
|
121
|
+
}
|
|
122
|
+
}, [currentStep, data]);
|
|
123
|
+
const goToNext = useCallback(async () => {
|
|
124
|
+
const isValid = await validateCurrentStep();
|
|
125
|
+
if (!isValid) return;
|
|
126
|
+
if (currentStepIndex === visibleSteps.length - 1) {
|
|
127
|
+
if (onComplete) {
|
|
128
|
+
setIsSubmitting(true);
|
|
129
|
+
try {
|
|
130
|
+
await onComplete(data);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error("Form submission error:", error);
|
|
133
|
+
setErrors({ _form: "Submission failed. Please try again." });
|
|
134
|
+
} finally {
|
|
135
|
+
setIsSubmitting(false);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
setCurrentStepIndex((prev) => Math.min(prev + 1, visibleSteps.length - 1));
|
|
141
|
+
setErrors({});
|
|
142
|
+
}, [currentStepIndex, visibleSteps.length, validateCurrentStep, onComplete, data]);
|
|
143
|
+
const goToPrevious = useCallback(() => {
|
|
144
|
+
setCurrentStepIndex((prev) => Math.max(prev - 1, 0));
|
|
145
|
+
setErrors({});
|
|
146
|
+
}, []);
|
|
147
|
+
const goToStep = useCallback(
|
|
148
|
+
(stepIndex) => {
|
|
149
|
+
if (stepIndex >= 0 && stepIndex < visibleSteps.length) {
|
|
150
|
+
setCurrentStepIndex(stepIndex);
|
|
151
|
+
setErrors({});
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
[visibleSteps.length]
|
|
155
|
+
);
|
|
156
|
+
const goToStepById = useCallback(
|
|
157
|
+
(stepId) => {
|
|
158
|
+
const stepIndex = visibleSteps.findIndex((s) => s.id === stepId);
|
|
159
|
+
if (stepIndex !== -1) {
|
|
160
|
+
goToStep(stepIndex);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[visibleSteps, goToStep]
|
|
164
|
+
);
|
|
165
|
+
const reset = useCallback(() => {
|
|
166
|
+
setData(initialData);
|
|
167
|
+
setCurrentStepIndex(0);
|
|
168
|
+
setErrors({});
|
|
169
|
+
setIsValidating(false);
|
|
170
|
+
setIsSubmitting(false);
|
|
171
|
+
}, [initialData]);
|
|
172
|
+
const canGoNext = !isValidating && !isSubmitting;
|
|
173
|
+
const canGoBack = currentStepIndex > 0 && !isValidating && !isSubmitting;
|
|
174
|
+
const isFirstStep = currentStepIndex === 0;
|
|
175
|
+
const isLastStep = currentStepIndex === visibleSteps.length - 1;
|
|
176
|
+
return {
|
|
177
|
+
currentStepIndex,
|
|
178
|
+
currentStepId,
|
|
179
|
+
visibleSteps,
|
|
180
|
+
data,
|
|
181
|
+
errors,
|
|
182
|
+
isValidating,
|
|
183
|
+
isSubmitting,
|
|
184
|
+
goToNext,
|
|
185
|
+
goToPrevious,
|
|
186
|
+
goToStep,
|
|
187
|
+
goToStepById,
|
|
188
|
+
updateData,
|
|
189
|
+
canGoNext,
|
|
190
|
+
canGoBack,
|
|
191
|
+
isFirstStep,
|
|
192
|
+
isLastStep,
|
|
193
|
+
reset
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ../blocks/src/system/runtime/components/multi-step/MultiStepForm.tsx
|
|
198
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
199
|
+
function MultiStepForm({
|
|
200
|
+
steps,
|
|
201
|
+
initialData,
|
|
202
|
+
onComplete,
|
|
203
|
+
onStepChange,
|
|
204
|
+
onDataChange,
|
|
205
|
+
progressStyle = "dots",
|
|
206
|
+
className = "",
|
|
207
|
+
persistToUrl = false,
|
|
208
|
+
allowSkip = false
|
|
209
|
+
}) {
|
|
210
|
+
const context = useMultiStep({
|
|
211
|
+
steps,
|
|
212
|
+
initialData,
|
|
213
|
+
onStepChange,
|
|
214
|
+
onDataChange,
|
|
215
|
+
onComplete,
|
|
216
|
+
persistToUrl
|
|
217
|
+
});
|
|
218
|
+
const {
|
|
219
|
+
visibleSteps,
|
|
220
|
+
currentStepIndex,
|
|
221
|
+
goToNext,
|
|
222
|
+
goToPrevious,
|
|
223
|
+
goToStep,
|
|
224
|
+
canGoNext,
|
|
225
|
+
canGoBack,
|
|
226
|
+
isFirstStep,
|
|
227
|
+
isLastStep,
|
|
228
|
+
errors,
|
|
229
|
+
isValidating,
|
|
230
|
+
isSubmitting
|
|
231
|
+
} = context;
|
|
232
|
+
const currentStep = visibleSteps[currentStepIndex];
|
|
233
|
+
if (!currentStep) {
|
|
234
|
+
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "No steps available" }) });
|
|
235
|
+
}
|
|
236
|
+
const stepComponent = isValidElement(currentStep.component) ? cloneElement(currentStep.component, {
|
|
237
|
+
// Step components can access context via hooks, but we also pass key props
|
|
238
|
+
stepId: currentStep.id,
|
|
239
|
+
stepIndex: currentStepIndex
|
|
240
|
+
}) : currentStep.component;
|
|
241
|
+
const showBackButton = !isFirstStep && !currentStep.hideBack && canGoBack;
|
|
242
|
+
const showNextButton = !currentStep.isTerminal;
|
|
243
|
+
const nextButtonLabel = isLastStep ? currentStep.nextLabel || "Complete" : currentStep.nextLabel || "Continue";
|
|
244
|
+
const backButtonLabel = currentStep.backLabel || "Back";
|
|
245
|
+
return /* @__PURE__ */ jsx(MultiStepContext.Provider, { value: context, children: /* @__PURE__ */ jsxs("div", { className: `multi-step-form ${className}`, children: [
|
|
246
|
+
progressStyle !== "none" && /* @__PURE__ */ jsx("div", { className: "mb-8", children: /* @__PURE__ */ jsx(
|
|
247
|
+
ProgressIndicator,
|
|
248
|
+
{
|
|
249
|
+
steps: visibleSteps,
|
|
250
|
+
currentIndex: currentStepIndex,
|
|
251
|
+
style: progressStyle,
|
|
252
|
+
onStepClick: allowSkip ? goToStep : void 0
|
|
253
|
+
}
|
|
254
|
+
) }),
|
|
255
|
+
/* @__PURE__ */ jsx("div", { className: "step-content", children: stepComponent }),
|
|
256
|
+
Object.keys(errors).length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6 alert alert-error", children: [
|
|
257
|
+
errors._form && /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: errors._form }),
|
|
258
|
+
Object.entries(errors).filter(([key]) => key !== "_form").map(([key, message]) => /* @__PURE__ */ jsx("p", { className: "text-sm", children: message }, key))
|
|
259
|
+
] }),
|
|
260
|
+
(showBackButton || showNextButton) && /* @__PURE__ */ jsxs("div", { className: "flex gap-3 mt-8", children: [
|
|
261
|
+
showBackButton && /* @__PURE__ */ jsx(
|
|
262
|
+
"button",
|
|
263
|
+
{
|
|
264
|
+
type: "button",
|
|
265
|
+
onClick: goToPrevious,
|
|
266
|
+
disabled: !canGoBack,
|
|
267
|
+
className: "outline btn-md",
|
|
268
|
+
children: backButtonLabel
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
showNextButton && /* @__PURE__ */ jsxs(
|
|
272
|
+
"button",
|
|
273
|
+
{
|
|
274
|
+
type: "button",
|
|
275
|
+
onClick: goToNext,
|
|
276
|
+
disabled: !canGoNext,
|
|
277
|
+
className: "primary btn-md flex-1",
|
|
278
|
+
children: [
|
|
279
|
+
isValidating && /* @__PURE__ */ jsx("span", { className: "inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-current" }),
|
|
280
|
+
isSubmitting && /* @__PURE__ */ jsx("span", { className: "inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-current" }),
|
|
281
|
+
!isValidating && !isSubmitting && nextButtonLabel,
|
|
282
|
+
isValidating && "Validating...",
|
|
283
|
+
isSubmitting && "Submitting..."
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
] })
|
|
288
|
+
] }) });
|
|
289
|
+
}
|
|
290
|
+
function ProgressIndicator({
|
|
291
|
+
steps,
|
|
292
|
+
currentIndex,
|
|
293
|
+
style,
|
|
294
|
+
onStepClick
|
|
295
|
+
}) {
|
|
296
|
+
if (style === "dots") {
|
|
297
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center gap-2", children: steps.map((step, index) => {
|
|
298
|
+
const isActive = index === currentIndex;
|
|
299
|
+
const isCompleted = index < currentIndex;
|
|
300
|
+
const isClickable = onStepClick && index <= currentIndex;
|
|
301
|
+
return /* @__PURE__ */ jsx(
|
|
302
|
+
"button",
|
|
303
|
+
{
|
|
304
|
+
type: "button",
|
|
305
|
+
onClick: isClickable ? () => onStepClick(index) : void 0,
|
|
306
|
+
disabled: !isClickable,
|
|
307
|
+
className: `
|
|
308
|
+
step-dot transition-all
|
|
309
|
+
${isActive ? "step-dot-active" : ""}
|
|
310
|
+
${isCompleted ? "step-dot-complete" : ""}
|
|
311
|
+
${isClickable ? "cursor-pointer hover:scale-125" : "cursor-default"}
|
|
312
|
+
`,
|
|
313
|
+
"aria-label": step.title
|
|
314
|
+
},
|
|
315
|
+
step.id
|
|
316
|
+
);
|
|
317
|
+
}) });
|
|
318
|
+
}
|
|
319
|
+
if (style === "steps") {
|
|
320
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between", children: steps.map((step, index) => {
|
|
321
|
+
const isActive = index === currentIndex;
|
|
322
|
+
const isCompleted = index < currentIndex;
|
|
323
|
+
const isClickable = onStepClick && index <= currentIndex;
|
|
324
|
+
const isLast = index === steps.length - 1;
|
|
325
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center flex-1", children: [
|
|
326
|
+
/* @__PURE__ */ jsxs(
|
|
327
|
+
"button",
|
|
328
|
+
{
|
|
329
|
+
type: "button",
|
|
330
|
+
onClick: isClickable ? () => onStepClick(index) : void 0,
|
|
331
|
+
disabled: !isClickable,
|
|
332
|
+
className: `
|
|
333
|
+
flex items-center gap-2
|
|
334
|
+
${isClickable ? "cursor-pointer" : "cursor-default"}
|
|
335
|
+
`,
|
|
336
|
+
children: [
|
|
337
|
+
/* @__PURE__ */ jsx(
|
|
338
|
+
"div",
|
|
339
|
+
{
|
|
340
|
+
className: `
|
|
341
|
+
step-circle transition-colors
|
|
342
|
+
${isActive ? "step-circle-active" : ""}
|
|
343
|
+
${isCompleted ? "step-circle-complete" : ""}
|
|
344
|
+
`,
|
|
345
|
+
children: isCompleted ? "\u2713" : index + 1
|
|
346
|
+
}
|
|
347
|
+
),
|
|
348
|
+
/* @__PURE__ */ jsx(
|
|
349
|
+
"span",
|
|
350
|
+
{
|
|
351
|
+
className: `
|
|
352
|
+
text-sm hidden sm:inline
|
|
353
|
+
${isActive ? "font-medium" : ""}
|
|
354
|
+
${isCompleted || !isActive ? "status-muted" : ""}
|
|
355
|
+
`,
|
|
356
|
+
children: step.title
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
),
|
|
362
|
+
!isLast && /* @__PURE__ */ jsx("div", { className: "step-connector mx-2 hidden sm:block" })
|
|
363
|
+
] }, step.id);
|
|
364
|
+
}) });
|
|
365
|
+
}
|
|
366
|
+
const progress = (currentIndex + 1) / steps.length * 100;
|
|
367
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
368
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm status-muted", children: [
|
|
369
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
370
|
+
"Step ",
|
|
371
|
+
currentIndex + 1,
|
|
372
|
+
" of ",
|
|
373
|
+
steps.length
|
|
374
|
+
] }),
|
|
375
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
376
|
+
Math.round(progress),
|
|
377
|
+
"%"
|
|
378
|
+
] })
|
|
379
|
+
] }),
|
|
380
|
+
/* @__PURE__ */ jsx("div", { className: "progress-bar", children: /* @__PURE__ */ jsx(
|
|
381
|
+
"div",
|
|
382
|
+
{
|
|
383
|
+
className: "progress-fill",
|
|
384
|
+
style: { width: `${progress}%` }
|
|
385
|
+
}
|
|
386
|
+
) })
|
|
387
|
+
] });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ../blocks/src/system/runtime/components/booking/SuccessMessage.tsx
|
|
391
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
392
|
+
var SuccessMessage = ({ message, className }) => {
|
|
393
|
+
return /* @__PURE__ */ jsx2(
|
|
394
|
+
"div",
|
|
395
|
+
{
|
|
396
|
+
className: `alert alert-success text-sm ${className ?? ""}`,
|
|
397
|
+
role: "alert",
|
|
398
|
+
"aria-live": "polite",
|
|
399
|
+
children: message
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// ../blocks/src/system/runtime/hooks/useBookingSteps.ts
|
|
405
|
+
import React4, { useMemo as useMemo3 } from "react";
|
|
406
|
+
|
|
407
|
+
// ../blocks/src/system/runtime/components/booking/ServiceResourceSelector.tsx
|
|
408
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
409
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
410
|
+
function ServiceResourceSelector({
|
|
411
|
+
siteId,
|
|
412
|
+
services,
|
|
413
|
+
preSelectedServiceId,
|
|
414
|
+
preSelectedResourceId,
|
|
415
|
+
onSelect,
|
|
416
|
+
onBack,
|
|
417
|
+
heading = "Select Service",
|
|
418
|
+
description = "Choose the service you'd like to book"
|
|
419
|
+
}) {
|
|
420
|
+
const [selectedServiceId, setSelectedServiceId] = useState2(
|
|
421
|
+
preSelectedServiceId
|
|
422
|
+
);
|
|
423
|
+
const [selectedResourceId, setSelectedResourceId] = useState2(
|
|
424
|
+
preSelectedResourceId
|
|
425
|
+
);
|
|
426
|
+
const [resources, setResources] = useState2([]);
|
|
427
|
+
const [isLoadingResources, setIsLoadingResources] = useState2(false);
|
|
428
|
+
const [resourceError, setResourceError] = useState2(null);
|
|
429
|
+
const showServiceSelect = !preSelectedServiceId && services.length > 1;
|
|
430
|
+
const showResourceSelect = resources.length > 1 && !preSelectedResourceId;
|
|
431
|
+
const handleServiceChange = useCallback2(async (serviceId) => {
|
|
432
|
+
setSelectedServiceId(serviceId);
|
|
433
|
+
setSelectedResourceId(void 0);
|
|
434
|
+
setResourceError(null);
|
|
435
|
+
try {
|
|
436
|
+
setIsLoadingResources(true);
|
|
437
|
+
const apiUrl = getCmsApiUrl();
|
|
438
|
+
const response = await fetch(
|
|
439
|
+
`${apiUrl}/sites/${siteId}/bookings/resources/reference?serviceId=${serviceId}`
|
|
440
|
+
);
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
throw new Error("Failed to load resources");
|
|
443
|
+
}
|
|
444
|
+
const data = await response.json();
|
|
445
|
+
const loadedResources = data.options?.map((opt) => ({
|
|
446
|
+
id: opt.id,
|
|
447
|
+
displayName: opt.label
|
|
448
|
+
})) || [];
|
|
449
|
+
setResources(loadedResources);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.error("Failed to load resources:", err);
|
|
452
|
+
setResourceError("Failed to load practitioners. Please try again.");
|
|
453
|
+
setResources([]);
|
|
454
|
+
} finally {
|
|
455
|
+
setIsLoadingResources(false);
|
|
456
|
+
}
|
|
457
|
+
}, [siteId]);
|
|
458
|
+
const handleContinue = () => {
|
|
459
|
+
if (selectedServiceId) {
|
|
460
|
+
onSelect({
|
|
461
|
+
serviceId: selectedServiceId,
|
|
462
|
+
resourceId: selectedResourceId === "__any__" ? void 0 : selectedResourceId
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const canContinue = selectedServiceId && !isLoadingResources;
|
|
467
|
+
useEffect2(() => {
|
|
468
|
+
if (services.length === 1 && !selectedServiceId) {
|
|
469
|
+
handleServiceChange(services[0].id);
|
|
470
|
+
}
|
|
471
|
+
}, [services, selectedServiceId, handleServiceChange]);
|
|
472
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-6 max-w-2xl mx-auto", children: [
|
|
473
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
474
|
+
/* @__PURE__ */ jsx3("h2", { className: "text-2xl font-bold", children: heading }),
|
|
475
|
+
description && /* @__PURE__ */ jsx3("p", { className: "text-muted-foreground mt-2", children: description })
|
|
476
|
+
] }),
|
|
477
|
+
showServiceSelect && /* @__PURE__ */ jsxs2("div", { className: "space-y-3", children: [
|
|
478
|
+
/* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Service" }),
|
|
479
|
+
/* @__PURE__ */ jsx3("div", { className: "grid gap-3", children: services.map((service) => /* @__PURE__ */ jsx3(
|
|
480
|
+
"button",
|
|
481
|
+
{
|
|
482
|
+
type: "button",
|
|
483
|
+
onClick: () => handleServiceChange(service.id),
|
|
484
|
+
className: `
|
|
485
|
+
relative p-4 rounded-lg border-2 text-left transition-all
|
|
486
|
+
${selectedServiceId === service.id ? "border-primary bg-primary/5" : "border-border hover:border-primary/50"}
|
|
487
|
+
`,
|
|
488
|
+
children: /* @__PURE__ */ jsxs2("div", { className: "flex items-start justify-between", children: [
|
|
489
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
490
|
+
/* @__PURE__ */ jsx3("div", { className: "font-medium", children: service.title }),
|
|
491
|
+
service.description && /* @__PURE__ */ jsx3("div", { className: "text-sm text-muted-foreground mt-1", children: service.description }),
|
|
492
|
+
service.durationMinutes && /* @__PURE__ */ jsxs2("div", { className: "text-sm text-muted-foreground mt-1", children: [
|
|
493
|
+
service.durationMinutes,
|
|
494
|
+
" minutes"
|
|
495
|
+
] })
|
|
496
|
+
] }),
|
|
497
|
+
selectedServiceId === service.id && /* @__PURE__ */ jsx3("div", { className: "flex-shrink-0 w-5 h-5 rounded-full bg-primary flex items-center justify-center", children: /* @__PURE__ */ jsx3(
|
|
498
|
+
"svg",
|
|
499
|
+
{
|
|
500
|
+
className: "w-3 h-3 text-primary-foreground",
|
|
501
|
+
fill: "none",
|
|
502
|
+
strokeLinecap: "round",
|
|
503
|
+
strokeLinejoin: "round",
|
|
504
|
+
strokeWidth: "2",
|
|
505
|
+
viewBox: "0 0 24 24",
|
|
506
|
+
stroke: "currentColor",
|
|
507
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M5 13l4 4L19 7" })
|
|
508
|
+
}
|
|
509
|
+
) })
|
|
510
|
+
] })
|
|
511
|
+
},
|
|
512
|
+
service.id
|
|
513
|
+
)) })
|
|
514
|
+
] }),
|
|
515
|
+
showResourceSelect && selectedServiceId && !isLoadingResources && /* @__PURE__ */ jsxs2("div", { className: "space-y-3", children: [
|
|
516
|
+
/* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Practitioner (Optional)" }),
|
|
517
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm text-muted-foreground", children: 'Choose a specific practitioner or select "Any Available"' }),
|
|
518
|
+
/* @__PURE__ */ jsxs2("div", { className: "grid gap-3", children: [
|
|
519
|
+
/* @__PURE__ */ jsx3(
|
|
520
|
+
"button",
|
|
521
|
+
{
|
|
522
|
+
type: "button",
|
|
523
|
+
onClick: () => setSelectedResourceId("__any__"),
|
|
524
|
+
className: `
|
|
525
|
+
relative p-4 rounded-lg border-2 text-left transition-all
|
|
526
|
+
${selectedResourceId === "__any__" ? "border-primary bg-primary/5" : "border-border hover:border-primary/50"}
|
|
527
|
+
`,
|
|
528
|
+
children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
|
|
529
|
+
/* @__PURE__ */ jsx3("div", { className: "font-medium", children: "Any Available" }),
|
|
530
|
+
selectedResourceId === "__any__" && /* @__PURE__ */ jsx3("div", { className: "flex-shrink-0 w-5 h-5 rounded-full bg-primary flex items-center justify-center", children: /* @__PURE__ */ jsx3(
|
|
531
|
+
"svg",
|
|
532
|
+
{
|
|
533
|
+
className: "w-3 h-3 text-primary-foreground",
|
|
534
|
+
fill: "none",
|
|
535
|
+
strokeLinecap: "round",
|
|
536
|
+
strokeLinejoin: "round",
|
|
537
|
+
strokeWidth: "2",
|
|
538
|
+
viewBox: "0 0 24 24",
|
|
539
|
+
stroke: "currentColor",
|
|
540
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M5 13l4 4L19 7" })
|
|
541
|
+
}
|
|
542
|
+
) })
|
|
543
|
+
] })
|
|
544
|
+
}
|
|
545
|
+
),
|
|
546
|
+
resources.map((resource) => /* @__PURE__ */ jsx3(
|
|
547
|
+
"button",
|
|
548
|
+
{
|
|
549
|
+
type: "button",
|
|
550
|
+
onClick: () => setSelectedResourceId(resource.id),
|
|
551
|
+
className: `
|
|
552
|
+
relative p-4 rounded-lg border-2 text-left transition-all
|
|
553
|
+
${selectedResourceId === resource.id ? "border-primary bg-primary/5" : "border-border hover:border-primary/50"}
|
|
554
|
+
`,
|
|
555
|
+
children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
|
|
556
|
+
/* @__PURE__ */ jsx3("div", { className: "font-medium", children: resource.displayName }),
|
|
557
|
+
selectedResourceId === resource.id && /* @__PURE__ */ jsx3("div", { className: "flex-shrink-0 w-5 h-5 rounded-full bg-primary flex items-center justify-center", children: /* @__PURE__ */ jsx3(
|
|
558
|
+
"svg",
|
|
559
|
+
{
|
|
560
|
+
className: "w-3 h-3 text-primary-foreground",
|
|
561
|
+
fill: "none",
|
|
562
|
+
strokeLinecap: "round",
|
|
563
|
+
strokeLinejoin: "round",
|
|
564
|
+
strokeWidth: "2",
|
|
565
|
+
viewBox: "0 0 24 24",
|
|
566
|
+
stroke: "currentColor",
|
|
567
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M5 13l4 4L19 7" })
|
|
568
|
+
}
|
|
569
|
+
) })
|
|
570
|
+
] })
|
|
571
|
+
},
|
|
572
|
+
resource.id
|
|
573
|
+
))
|
|
574
|
+
] })
|
|
575
|
+
] }),
|
|
576
|
+
isLoadingResources && /* @__PURE__ */ jsxs2("div", { className: "text-center py-8", children: [
|
|
577
|
+
/* @__PURE__ */ jsx3("div", { className: "inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary" }),
|
|
578
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm text-muted-foreground mt-2", children: "Loading practitioners..." })
|
|
579
|
+
] }),
|
|
580
|
+
resourceError && /* @__PURE__ */ jsx3("div", { className: "alert alert-error text-sm", children: resourceError }),
|
|
581
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex gap-3", children: [
|
|
582
|
+
onBack && /* @__PURE__ */ jsx3(
|
|
583
|
+
"button",
|
|
584
|
+
{
|
|
585
|
+
type: "button",
|
|
586
|
+
onClick: onBack,
|
|
587
|
+
className: "px-6 py-2 rounded-lg border border-border hover:bg-muted transition-colors",
|
|
588
|
+
children: "Back"
|
|
589
|
+
}
|
|
590
|
+
),
|
|
591
|
+
/* @__PURE__ */ jsx3(
|
|
592
|
+
"button",
|
|
593
|
+
{
|
|
594
|
+
type: "button",
|
|
595
|
+
onClick: handleContinue,
|
|
596
|
+
disabled: !canContinue,
|
|
597
|
+
className: "flex-1 px-6 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
|
598
|
+
children: "Continue to Date Selection"
|
|
599
|
+
}
|
|
600
|
+
)
|
|
601
|
+
] })
|
|
602
|
+
] });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ../blocks/src/system/runtime/components/booking/ServiceSelectionStep.tsx
|
|
606
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
607
|
+
function ServiceSelectionStep({
|
|
608
|
+
siteId,
|
|
609
|
+
services
|
|
610
|
+
}) {
|
|
611
|
+
const { updateData } = useMultiStepData();
|
|
612
|
+
return /* @__PURE__ */ jsx4(
|
|
613
|
+
ServiceResourceSelector,
|
|
614
|
+
{
|
|
615
|
+
siteId,
|
|
616
|
+
services,
|
|
617
|
+
onSelect: ({ serviceId, resourceId }) => {
|
|
618
|
+
updateData({ serviceId, resourceId });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ../blocks/src/system/runtime/components/booking/DateTimeSelectionStep.tsx
|
|
625
|
+
import { useEffect as useEffect5, useMemo as useMemo2 } from "react";
|
|
626
|
+
|
|
627
|
+
// ../blocks/src/utils/date-formatting.ts
|
|
628
|
+
var BOOKING_FETCH_CHUNK_DAYS = 30;
|
|
629
|
+
function formatDate(dateStr) {
|
|
630
|
+
const date = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
631
|
+
return date.toLocaleDateString(void 0, {
|
|
632
|
+
weekday: "long",
|
|
633
|
+
year: "numeric",
|
|
634
|
+
month: "long",
|
|
635
|
+
day: "numeric"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function formatTime(isoString) {
|
|
639
|
+
const date = new Date(isoString);
|
|
640
|
+
return date.toLocaleTimeString(void 0, {
|
|
641
|
+
hour: "numeric",
|
|
642
|
+
minute: "2-digit",
|
|
643
|
+
timeZoneName: "short"
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ../blocks/src/system/runtime/components/booking/DatePicker.tsx
|
|
648
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
649
|
+
var DatePicker = ({
|
|
650
|
+
value,
|
|
651
|
+
onChange,
|
|
652
|
+
dateOptions,
|
|
653
|
+
required = true,
|
|
654
|
+
isLoading = false,
|
|
655
|
+
isEmpty = false,
|
|
656
|
+
hasMore = false,
|
|
657
|
+
onLoadMore
|
|
658
|
+
}) => {
|
|
659
|
+
if (isLoading) {
|
|
660
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
661
|
+
/* @__PURE__ */ jsxs3("label", { className: "form-label", children: [
|
|
662
|
+
"Select Date",
|
|
663
|
+
required && /* @__PURE__ */ jsx5("span", { className: "required-marker", children: "*" })
|
|
664
|
+
] }),
|
|
665
|
+
/* @__PURE__ */ jsx5("div", { className: "h-10 w-full animate-pulse rounded-control card-surface" }),
|
|
666
|
+
/* @__PURE__ */ jsx5("p", { className: "text-xs status-muted", children: "Loading available dates..." })
|
|
667
|
+
] });
|
|
668
|
+
}
|
|
669
|
+
if (isEmpty) {
|
|
670
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
671
|
+
/* @__PURE__ */ jsxs3("label", { className: "form-label", children: [
|
|
672
|
+
"Select Date",
|
|
673
|
+
required && /* @__PURE__ */ jsx5("span", { className: "required-marker", children: "*" })
|
|
674
|
+
] }),
|
|
675
|
+
/* @__PURE__ */ jsx5("div", { className: "alert alert-warning", children: /* @__PURE__ */ jsxs3("p", { className: "text-sm", children: [
|
|
676
|
+
"No available dates in this period.",
|
|
677
|
+
hasMore && onLoadMore && /* @__PURE__ */ jsx5(
|
|
678
|
+
"button",
|
|
679
|
+
{
|
|
680
|
+
type: "button",
|
|
681
|
+
onClick: onLoadMore,
|
|
682
|
+
className: "ml-2 underline hover:no-underline",
|
|
683
|
+
children: "Check later dates"
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
] }) })
|
|
687
|
+
] });
|
|
688
|
+
}
|
|
689
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
690
|
+
/* @__PURE__ */ jsxs3("label", { htmlFor: "booking-date", className: "form-label", children: [
|
|
691
|
+
"Select Date",
|
|
692
|
+
required && /* @__PURE__ */ jsx5("span", { className: "required-marker", children: "*" })
|
|
693
|
+
] }),
|
|
694
|
+
/* @__PURE__ */ jsxs3(
|
|
695
|
+
"select",
|
|
696
|
+
{
|
|
697
|
+
id: "booking-date",
|
|
698
|
+
value,
|
|
699
|
+
onChange: (e) => onChange(e.target.value),
|
|
700
|
+
required,
|
|
701
|
+
className: "form-select",
|
|
702
|
+
children: [
|
|
703
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: "Choose a date..." }),
|
|
704
|
+
dateOptions.map((date) => /* @__PURE__ */ jsx5("option", { value: date, children: formatDate(date) }, date))
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
),
|
|
708
|
+
hasMore && onLoadMore && /* @__PURE__ */ jsx5(
|
|
709
|
+
"button",
|
|
710
|
+
{
|
|
711
|
+
type: "button",
|
|
712
|
+
onClick: onLoadMore,
|
|
713
|
+
className: "button-link text-sm",
|
|
714
|
+
children: "Load more dates \u2192"
|
|
715
|
+
}
|
|
716
|
+
)
|
|
717
|
+
] });
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// ../blocks/src/system/runtime/components/booking/TimeSlotSelector.tsx
|
|
721
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
722
|
+
var TimeSlotSelector = ({
|
|
723
|
+
value,
|
|
724
|
+
onChange,
|
|
725
|
+
slots,
|
|
726
|
+
isLoading,
|
|
727
|
+
required = true
|
|
728
|
+
}) => {
|
|
729
|
+
return /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
|
|
730
|
+
/* @__PURE__ */ jsxs4("label", { htmlFor: "booking-slot", className: "form-label", children: [
|
|
731
|
+
"Select Time",
|
|
732
|
+
required && /* @__PURE__ */ jsx6("span", { className: "required-marker", children: "*" })
|
|
733
|
+
] }),
|
|
734
|
+
isLoading ? /* @__PURE__ */ jsx6("div", { className: "alert alert-info text-sm text-center", children: "Loading available times..." }) : slots.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "alert text-sm text-center status-muted", children: "No available times for this date" }) : /* @__PURE__ */ jsxs4(
|
|
735
|
+
"select",
|
|
736
|
+
{
|
|
737
|
+
id: "booking-slot",
|
|
738
|
+
value,
|
|
739
|
+
onChange: (e) => onChange(e.target.value),
|
|
740
|
+
required,
|
|
741
|
+
className: "form-select",
|
|
742
|
+
children: [
|
|
743
|
+
/* @__PURE__ */ jsx6("option", { value: "", children: "Choose a time..." }),
|
|
744
|
+
slots.map((slot) => /* @__PURE__ */ jsx6("option", { value: slot.startAt, children: formatTime(slot.startAt) }, slot.startAt))
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
)
|
|
748
|
+
] });
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// ../blocks/src/system/runtime/hooks/useAvailableSlots.ts
|
|
752
|
+
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
753
|
+
function useAvailableSlots({
|
|
754
|
+
siteId,
|
|
755
|
+
serviceId,
|
|
756
|
+
resourceId,
|
|
757
|
+
selectedDate,
|
|
758
|
+
timezone
|
|
759
|
+
}) {
|
|
760
|
+
const [slots, setSlots] = useState3([]);
|
|
761
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
762
|
+
const [error, setError] = useState3(null);
|
|
763
|
+
useEffect3(() => {
|
|
764
|
+
if (!selectedDate || !serviceId) {
|
|
765
|
+
setSlots([]);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const fetchSlots = async () => {
|
|
769
|
+
setIsLoading(true);
|
|
770
|
+
setError(null);
|
|
771
|
+
setSlots([]);
|
|
772
|
+
try {
|
|
773
|
+
const params = new URLSearchParams({
|
|
774
|
+
siteId,
|
|
775
|
+
serviceId,
|
|
776
|
+
startDate: selectedDate,
|
|
777
|
+
endDate: selectedDate,
|
|
778
|
+
timezone
|
|
779
|
+
});
|
|
780
|
+
if (resourceId) {
|
|
781
|
+
params.append("resourceId", resourceId);
|
|
782
|
+
}
|
|
783
|
+
const apiUrl = getCmsApiUrl();
|
|
784
|
+
const response = await fetch(`${apiUrl}/public/bookings/availability/slots?${params}`, {
|
|
785
|
+
method: "GET",
|
|
786
|
+
headers: {
|
|
787
|
+
"Content-Type": "application/json"
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
if (!response.ok) {
|
|
791
|
+
const errorData = await response.json().catch(() => ({}));
|
|
792
|
+
throw new Error(errorData.error || "Failed to load available times");
|
|
793
|
+
}
|
|
794
|
+
const data = await response.json();
|
|
795
|
+
setSlots(data.slots);
|
|
796
|
+
} catch (err) {
|
|
797
|
+
setError(err instanceof Error ? err.message : "Failed to load available times");
|
|
798
|
+
} finally {
|
|
799
|
+
setIsLoading(false);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
fetchSlots();
|
|
803
|
+
}, [selectedDate, serviceId, resourceId, timezone, siteId]);
|
|
804
|
+
return { slots, isLoading, error };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ../blocks/src/system/runtime/hooks/useAvailableDates.ts
|
|
808
|
+
import { useState as useState4, useEffect as useEffect4, useCallback as useCallback3, useRef } from "react";
|
|
809
|
+
function useAvailableDates({
|
|
810
|
+
siteId,
|
|
811
|
+
serviceId,
|
|
812
|
+
resourceId,
|
|
813
|
+
timezone = "UTC",
|
|
814
|
+
initialDays = 30
|
|
815
|
+
}) {
|
|
816
|
+
const [availableDates, setAvailableDates] = useState4(/* @__PURE__ */ new Set());
|
|
817
|
+
const [isLoading, setIsLoading] = useState4(false);
|
|
818
|
+
const [error, setError] = useState4(null);
|
|
819
|
+
const [hasMore, setHasMore] = useState4(false);
|
|
820
|
+
const [loadedRange, setLoadedRange] = useState4(null);
|
|
821
|
+
const nextStartDateRef = useRef(null);
|
|
822
|
+
const requestIdRef = useRef(0);
|
|
823
|
+
const fetchDates = useCallback3(
|
|
824
|
+
async (startDate, endDate, append = false) => {
|
|
825
|
+
if (!serviceId) {
|
|
826
|
+
console.log("[useAvailableDates] No serviceId, skipping fetch");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const requestId = ++requestIdRef.current;
|
|
830
|
+
setIsLoading(true);
|
|
831
|
+
setError(null);
|
|
832
|
+
try {
|
|
833
|
+
const apiUrl = getCmsApiUrl();
|
|
834
|
+
const params = new URLSearchParams({
|
|
835
|
+
siteId,
|
|
836
|
+
serviceId,
|
|
837
|
+
startDate,
|
|
838
|
+
endDate,
|
|
839
|
+
timezone
|
|
840
|
+
});
|
|
841
|
+
if (resourceId) {
|
|
842
|
+
params.append("resourceId", resourceId);
|
|
843
|
+
}
|
|
844
|
+
const url = `${apiUrl}/public/bookings/availability/dates?${params}`;
|
|
845
|
+
console.log("[useAvailableDates] Fetching:", url);
|
|
846
|
+
const res = await fetch(url, {
|
|
847
|
+
method: "GET",
|
|
848
|
+
headers: {
|
|
849
|
+
"Content-Type": "application/json"
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
console.log("[useAvailableDates] Response status:", res.status);
|
|
853
|
+
if (requestId !== requestIdRef.current) return;
|
|
854
|
+
if (!res.ok) {
|
|
855
|
+
const errorData = await res.json().catch(() => ({}));
|
|
856
|
+
console.log("[useAvailableDates] Error response:", errorData);
|
|
857
|
+
throw new Error(errorData.error || "Failed to fetch available dates");
|
|
858
|
+
}
|
|
859
|
+
const data = await res.json();
|
|
860
|
+
console.log("[useAvailableDates] Received dates:", data);
|
|
861
|
+
setAvailableDates((prev) => {
|
|
862
|
+
const newSet = append ? new Set(prev) : /* @__PURE__ */ new Set();
|
|
863
|
+
data.dates.forEach((d) => newSet.add(d));
|
|
864
|
+
console.log("[useAvailableDates] Updated availableDates Set size:", newSet.size);
|
|
865
|
+
return newSet;
|
|
866
|
+
});
|
|
867
|
+
setHasMore(data.hasMore);
|
|
868
|
+
nextStartDateRef.current = data.nextStartDate || null;
|
|
869
|
+
setLoadedRange((prev) => ({
|
|
870
|
+
start: append && prev ? prev.start : data.startDate,
|
|
871
|
+
end: data.endDate
|
|
872
|
+
}));
|
|
873
|
+
} catch (err) {
|
|
874
|
+
if (requestId !== requestIdRef.current) return;
|
|
875
|
+
setError(err instanceof Error ? err.message : "Failed to load available dates");
|
|
876
|
+
} finally {
|
|
877
|
+
if (requestId === requestIdRef.current) {
|
|
878
|
+
setIsLoading(false);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
[siteId, serviceId, resourceId, timezone]
|
|
883
|
+
);
|
|
884
|
+
useEffect4(() => {
|
|
885
|
+
if (!serviceId) {
|
|
886
|
+
setAvailableDates(/* @__PURE__ */ new Set());
|
|
887
|
+
setLoadedRange(null);
|
|
888
|
+
setHasMore(false);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
892
|
+
const endDate = new Date(Date.now() + initialDays * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
|
|
893
|
+
fetchDates(today, endDate, false);
|
|
894
|
+
}, [serviceId, fetchDates, initialDays]);
|
|
895
|
+
const loadMore = useCallback3(() => {
|
|
896
|
+
if (!nextStartDateRef.current || isLoading) return;
|
|
897
|
+
const start = nextStartDateRef.current;
|
|
898
|
+
const msPerDay = 24 * 60 * 60 * 1e3;
|
|
899
|
+
const end = new Date(new Date(start).getTime() + BOOKING_FETCH_CHUNK_DAYS * msPerDay).toISOString().split("T")[0];
|
|
900
|
+
fetchDates(start, end, true);
|
|
901
|
+
}, [fetchDates, isLoading]);
|
|
902
|
+
return {
|
|
903
|
+
availableDates,
|
|
904
|
+
isLoading,
|
|
905
|
+
error,
|
|
906
|
+
hasMore,
|
|
907
|
+
loadMore,
|
|
908
|
+
loadedRange
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ../blocks/src/system/runtime/components/booking/DateTimeSelectionStep.tsx
|
|
913
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
914
|
+
function DateTimeSelectionStep({
|
|
915
|
+
siteId,
|
|
916
|
+
preSelectedServiceId,
|
|
917
|
+
preSelectedResourceId,
|
|
918
|
+
singleService
|
|
919
|
+
}) {
|
|
920
|
+
const { data, updateData } = useMultiStepData();
|
|
921
|
+
const serviceId = data.serviceId || preSelectedServiceId;
|
|
922
|
+
const resourceId = data.resourceId || preSelectedResourceId;
|
|
923
|
+
const selectedDate = data.selectedDate || "";
|
|
924
|
+
const selectedSlot = data.selectedSlot || "";
|
|
925
|
+
useEffect5(() => {
|
|
926
|
+
if (preSelectedServiceId && !data.serviceId) {
|
|
927
|
+
updateData({ serviceId: preSelectedServiceId });
|
|
928
|
+
}
|
|
929
|
+
}, [preSelectedServiceId, data.serviceId, updateData]);
|
|
930
|
+
const {
|
|
931
|
+
availableDates,
|
|
932
|
+
isLoading: isLoadingDates,
|
|
933
|
+
error: datesError,
|
|
934
|
+
hasMore,
|
|
935
|
+
loadMore
|
|
936
|
+
} = useAvailableDates({
|
|
937
|
+
siteId,
|
|
938
|
+
serviceId,
|
|
939
|
+
resourceId,
|
|
940
|
+
timezone: "UTC",
|
|
941
|
+
initialDays: 30
|
|
942
|
+
});
|
|
943
|
+
const { slots, isLoading: isLoadingSlots, error: slotsError } = useAvailableSlots({
|
|
944
|
+
siteId,
|
|
945
|
+
serviceId,
|
|
946
|
+
resourceId,
|
|
947
|
+
selectedDate,
|
|
948
|
+
timezone: "UTC"
|
|
949
|
+
});
|
|
950
|
+
const filteredDateOptions = useMemo2(() => {
|
|
951
|
+
if (isLoadingDates || availableDates.size === 0) {
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
return [...availableDates].sort();
|
|
955
|
+
}, [availableDates, isLoadingDates]);
|
|
956
|
+
const handleDateChange = (date) => {
|
|
957
|
+
updateData({ selectedDate: date, selectedSlot: "" });
|
|
958
|
+
};
|
|
959
|
+
const handleSlotChange = (slot) => {
|
|
960
|
+
updateData({ selectedSlot: slot });
|
|
961
|
+
};
|
|
962
|
+
const error = datesError || slotsError;
|
|
963
|
+
return /* @__PURE__ */ jsxs5("div", { className: "space-y-6", children: [
|
|
964
|
+
singleService && /* @__PURE__ */ jsxs5("div", { className: "rounded-lg border border-border bg-muted/30 p-4", children: [
|
|
965
|
+
/* @__PURE__ */ jsx7("h3", { className: "font-medium text-foreground", children: singleService.title }),
|
|
966
|
+
singleService.description && /* @__PURE__ */ jsx7("p", { className: "mt-1 text-sm text-muted-foreground", children: singleService.description }),
|
|
967
|
+
singleService.durationMinutes && /* @__PURE__ */ jsxs5("p", { className: "mt-1 text-sm text-muted-foreground", children: [
|
|
968
|
+
"Duration: ",
|
|
969
|
+
singleService.durationMinutes,
|
|
970
|
+
" minutes"
|
|
971
|
+
] })
|
|
972
|
+
] }),
|
|
973
|
+
error && /* @__PURE__ */ jsx7("div", { className: "alert alert-error text-sm", children: error }),
|
|
974
|
+
/* @__PURE__ */ jsx7(
|
|
975
|
+
DatePicker,
|
|
976
|
+
{
|
|
977
|
+
value: selectedDate,
|
|
978
|
+
onChange: handleDateChange,
|
|
979
|
+
dateOptions: filteredDateOptions,
|
|
980
|
+
isLoading: isLoadingDates,
|
|
981
|
+
isEmpty: !isLoadingDates && filteredDateOptions.length === 0,
|
|
982
|
+
hasMore,
|
|
983
|
+
onLoadMore: loadMore
|
|
984
|
+
}
|
|
985
|
+
),
|
|
986
|
+
selectedDate && /* @__PURE__ */ jsx7(
|
|
987
|
+
TimeSlotSelector,
|
|
988
|
+
{
|
|
989
|
+
value: selectedSlot,
|
|
990
|
+
onChange: handleSlotChange,
|
|
991
|
+
slots,
|
|
992
|
+
isLoading: isLoadingSlots
|
|
993
|
+
}
|
|
994
|
+
)
|
|
995
|
+
] });
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ../blocks/src/system/runtime/components/multi-step/DynamicFormFields.tsx
|
|
999
|
+
import { useState as useState5, useCallback as useCallback4 } from "react";
|
|
1000
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1001
|
+
function DynamicFormFields({
|
|
1002
|
+
fields,
|
|
1003
|
+
showLabels = true,
|
|
1004
|
+
className = ""
|
|
1005
|
+
}) {
|
|
1006
|
+
const { data, updateData } = useMultiStepData();
|
|
1007
|
+
const [touched, setTouched] = useState5({});
|
|
1008
|
+
const handleChange = useCallback4(
|
|
1009
|
+
(fieldId, value) => {
|
|
1010
|
+
updateData({ [fieldId]: value });
|
|
1011
|
+
},
|
|
1012
|
+
[updateData]
|
|
1013
|
+
);
|
|
1014
|
+
const handleBlur = useCallback4((fieldId) => {
|
|
1015
|
+
setTouched((prev) => ({ ...prev, [fieldId]: true }));
|
|
1016
|
+
}, []);
|
|
1017
|
+
const getFieldError = useCallback4(
|
|
1018
|
+
(field) => {
|
|
1019
|
+
const value = data[field.id];
|
|
1020
|
+
const isTouched = touched[field.id];
|
|
1021
|
+
if (!isTouched) return null;
|
|
1022
|
+
if (field.required && !value) {
|
|
1023
|
+
return `${field.label} is required`;
|
|
1024
|
+
}
|
|
1025
|
+
if (value) {
|
|
1026
|
+
switch (field.type) {
|
|
1027
|
+
case "email":
|
|
1028
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
1029
|
+
return "Please enter a valid email address";
|
|
1030
|
+
}
|
|
1031
|
+
break;
|
|
1032
|
+
case "url":
|
|
1033
|
+
try {
|
|
1034
|
+
new URL(value);
|
|
1035
|
+
} catch {
|
|
1036
|
+
return "Please enter a valid URL";
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
case "tel":
|
|
1040
|
+
if (!/^[+\d\s\-()]+$/.test(value)) {
|
|
1041
|
+
return "Please enter a valid phone number";
|
|
1042
|
+
}
|
|
1043
|
+
break;
|
|
1044
|
+
case "number":
|
|
1045
|
+
const num = Number(value);
|
|
1046
|
+
if (isNaN(num)) {
|
|
1047
|
+
return "Please enter a valid number";
|
|
1048
|
+
}
|
|
1049
|
+
if (field.min !== void 0 && num < field.min) {
|
|
1050
|
+
return `Value must be at least ${field.min}`;
|
|
1051
|
+
}
|
|
1052
|
+
if (field.max !== void 0 && num > field.max) {
|
|
1053
|
+
return `Value must be at most ${field.max}`;
|
|
1054
|
+
}
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
if (typeof value === "string") {
|
|
1058
|
+
if (field.minLength && value.length < field.minLength) {
|
|
1059
|
+
return `Must be at least ${field.minLength} characters`;
|
|
1060
|
+
}
|
|
1061
|
+
if (field.maxLength && value.length > field.maxLength) {
|
|
1062
|
+
return `Must be at most ${field.maxLength} characters`;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (field.pattern && typeof value === "string") {
|
|
1066
|
+
const regex = new RegExp(field.pattern);
|
|
1067
|
+
if (!regex.test(value)) {
|
|
1068
|
+
return "Please match the required format";
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return null;
|
|
1073
|
+
},
|
|
1074
|
+
[data, touched]
|
|
1075
|
+
);
|
|
1076
|
+
return /* @__PURE__ */ jsx8("div", { className: `space-y-6 ${className}`, children: fields.map((field) => {
|
|
1077
|
+
const error = getFieldError(field);
|
|
1078
|
+
const value = data[field.id] ?? "";
|
|
1079
|
+
return /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1080
|
+
showLabels && /* @__PURE__ */ jsxs6("label", { htmlFor: field.id, className: "form-label", children: [
|
|
1081
|
+
field.label,
|
|
1082
|
+
field.required && /* @__PURE__ */ jsx8("span", { className: "required-marker", children: "*" })
|
|
1083
|
+
] }),
|
|
1084
|
+
field.helpText && /* @__PURE__ */ jsx8("p", { className: "text-sm status-muted", children: field.helpText }),
|
|
1085
|
+
/* @__PURE__ */ jsx8(
|
|
1086
|
+
FieldInput,
|
|
1087
|
+
{
|
|
1088
|
+
field,
|
|
1089
|
+
value,
|
|
1090
|
+
onChange: (val) => handleChange(field.id, val),
|
|
1091
|
+
onBlur: () => handleBlur(field.id),
|
|
1092
|
+
error
|
|
1093
|
+
}
|
|
1094
|
+
),
|
|
1095
|
+
error && /* @__PURE__ */ jsx8("p", { className: "text-sm status-error", children: error })
|
|
1096
|
+
] }, field.id);
|
|
1097
|
+
}) });
|
|
1098
|
+
}
|
|
1099
|
+
function FieldInput({
|
|
1100
|
+
field,
|
|
1101
|
+
value,
|
|
1102
|
+
onChange,
|
|
1103
|
+
onBlur,
|
|
1104
|
+
error
|
|
1105
|
+
}) {
|
|
1106
|
+
const inputClass = "form-input";
|
|
1107
|
+
const textareaClass = "form-textarea";
|
|
1108
|
+
const selectClass = "form-select";
|
|
1109
|
+
const checkboxClass = "form-checkbox";
|
|
1110
|
+
const radioClass = "form-radio";
|
|
1111
|
+
switch (field.type) {
|
|
1112
|
+
case "textarea":
|
|
1113
|
+
return /* @__PURE__ */ jsx8(
|
|
1114
|
+
"textarea",
|
|
1115
|
+
{
|
|
1116
|
+
id: field.id,
|
|
1117
|
+
name: field.id,
|
|
1118
|
+
value,
|
|
1119
|
+
onChange: (e) => onChange(e.target.value),
|
|
1120
|
+
onBlur,
|
|
1121
|
+
placeholder: field.placeholder,
|
|
1122
|
+
required: field.required,
|
|
1123
|
+
minLength: field.minLength,
|
|
1124
|
+
maxLength: field.maxLength,
|
|
1125
|
+
rows: 4,
|
|
1126
|
+
"aria-invalid": error ? "true" : void 0,
|
|
1127
|
+
className: textareaClass
|
|
1128
|
+
}
|
|
1129
|
+
);
|
|
1130
|
+
case "select":
|
|
1131
|
+
if (field.multiple) {
|
|
1132
|
+
return /* @__PURE__ */ jsx8(
|
|
1133
|
+
"select",
|
|
1134
|
+
{
|
|
1135
|
+
id: field.id,
|
|
1136
|
+
name: field.id,
|
|
1137
|
+
value: Array.isArray(value) ? value : [],
|
|
1138
|
+
onChange: (e) => {
|
|
1139
|
+
const selected = Array.from(e.target.selectedOptions, (opt) => opt.value);
|
|
1140
|
+
onChange(selected);
|
|
1141
|
+
},
|
|
1142
|
+
onBlur,
|
|
1143
|
+
required: field.required,
|
|
1144
|
+
multiple: true,
|
|
1145
|
+
"aria-invalid": error ? "true" : void 0,
|
|
1146
|
+
className: `${selectClass} h-32`,
|
|
1147
|
+
children: field.options?.map((opt) => /* @__PURE__ */ jsx8("option", { value: opt.value, children: opt.label }, opt.value))
|
|
1148
|
+
}
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
return /* @__PURE__ */ jsxs6(
|
|
1152
|
+
"select",
|
|
1153
|
+
{
|
|
1154
|
+
id: field.id,
|
|
1155
|
+
name: field.id,
|
|
1156
|
+
value,
|
|
1157
|
+
onChange: (e) => onChange(e.target.value),
|
|
1158
|
+
onBlur,
|
|
1159
|
+
required: field.required,
|
|
1160
|
+
"aria-invalid": error ? "true" : void 0,
|
|
1161
|
+
className: selectClass,
|
|
1162
|
+
children: [
|
|
1163
|
+
/* @__PURE__ */ jsx8("option", { value: "", children: "Select an option..." }),
|
|
1164
|
+
field.options?.map((opt) => /* @__PURE__ */ jsx8("option", { value: opt.value, children: opt.label }, opt.value))
|
|
1165
|
+
]
|
|
1166
|
+
}
|
|
1167
|
+
);
|
|
1168
|
+
case "radio":
|
|
1169
|
+
return /* @__PURE__ */ jsx8("div", { className: "space-y-2", children: field.options?.map((opt) => /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
1170
|
+
/* @__PURE__ */ jsx8(
|
|
1171
|
+
"input",
|
|
1172
|
+
{
|
|
1173
|
+
type: "radio",
|
|
1174
|
+
name: field.id,
|
|
1175
|
+
value: opt.value,
|
|
1176
|
+
checked: value === opt.value,
|
|
1177
|
+
onChange: (e) => onChange(e.target.value),
|
|
1178
|
+
onBlur,
|
|
1179
|
+
required: field.required,
|
|
1180
|
+
className: radioClass
|
|
1181
|
+
}
|
|
1182
|
+
),
|
|
1183
|
+
/* @__PURE__ */ jsx8("span", { className: "text-sm", children: opt.label })
|
|
1184
|
+
] }, opt.value)) });
|
|
1185
|
+
case "checkbox":
|
|
1186
|
+
if (field.options && field.options.length > 1) {
|
|
1187
|
+
const checkedValues = Array.isArray(value) ? value : [];
|
|
1188
|
+
return /* @__PURE__ */ jsx8("div", { className: "space-y-2", children: field.options.map((opt) => /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
1189
|
+
/* @__PURE__ */ jsx8(
|
|
1190
|
+
"input",
|
|
1191
|
+
{
|
|
1192
|
+
type: "checkbox",
|
|
1193
|
+
name: field.id,
|
|
1194
|
+
value: opt.value,
|
|
1195
|
+
checked: checkedValues.includes(opt.value),
|
|
1196
|
+
onChange: (e) => {
|
|
1197
|
+
const newValues = e.target.checked ? [...checkedValues, opt.value] : checkedValues.filter((v) => v !== opt.value);
|
|
1198
|
+
onChange(newValues);
|
|
1199
|
+
},
|
|
1200
|
+
onBlur,
|
|
1201
|
+
className: checkboxClass
|
|
1202
|
+
}
|
|
1203
|
+
),
|
|
1204
|
+
/* @__PURE__ */ jsx8("span", { className: "text-sm", children: opt.label })
|
|
1205
|
+
] }, opt.value)) });
|
|
1206
|
+
}
|
|
1207
|
+
return /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
1208
|
+
/* @__PURE__ */ jsx8(
|
|
1209
|
+
"input",
|
|
1210
|
+
{
|
|
1211
|
+
type: "checkbox",
|
|
1212
|
+
id: field.id,
|
|
1213
|
+
name: field.id,
|
|
1214
|
+
checked: !!value,
|
|
1215
|
+
onChange: (e) => onChange(e.target.checked),
|
|
1216
|
+
onBlur,
|
|
1217
|
+
required: field.required,
|
|
1218
|
+
className: checkboxClass
|
|
1219
|
+
}
|
|
1220
|
+
),
|
|
1221
|
+
/* @__PURE__ */ jsx8("span", { className: "text-sm", children: field.placeholder || field.label })
|
|
1222
|
+
] });
|
|
1223
|
+
case "consent":
|
|
1224
|
+
return /* @__PURE__ */ jsxs6("label", { className: "flex items-start gap-2 cursor-pointer", children: [
|
|
1225
|
+
/* @__PURE__ */ jsx8(
|
|
1226
|
+
"input",
|
|
1227
|
+
{
|
|
1228
|
+
type: "checkbox",
|
|
1229
|
+
id: field.id,
|
|
1230
|
+
name: field.id,
|
|
1231
|
+
checked: !!value,
|
|
1232
|
+
onChange: (e) => onChange(e.target.checked),
|
|
1233
|
+
onBlur,
|
|
1234
|
+
required: field.required,
|
|
1235
|
+
className: `${checkboxClass} mt-0.5`
|
|
1236
|
+
}
|
|
1237
|
+
),
|
|
1238
|
+
/* @__PURE__ */ jsx8("span", { className: "text-sm", children: field.placeholder || field.label })
|
|
1239
|
+
] });
|
|
1240
|
+
case "number":
|
|
1241
|
+
return /* @__PURE__ */ jsx8(
|
|
1242
|
+
"input",
|
|
1243
|
+
{
|
|
1244
|
+
type: "number",
|
|
1245
|
+
id: field.id,
|
|
1246
|
+
name: field.id,
|
|
1247
|
+
value,
|
|
1248
|
+
onChange: (e) => onChange(e.target.value),
|
|
1249
|
+
onBlur,
|
|
1250
|
+
placeholder: field.placeholder,
|
|
1251
|
+
required: field.required,
|
|
1252
|
+
min: field.min,
|
|
1253
|
+
max: field.max,
|
|
1254
|
+
"aria-invalid": error ? "true" : void 0,
|
|
1255
|
+
className: inputClass
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
case "date":
|
|
1259
|
+
return /* @__PURE__ */ jsx8(
|
|
1260
|
+
"input",
|
|
1261
|
+
{
|
|
1262
|
+
type: "date",
|
|
1263
|
+
id: field.id,
|
|
1264
|
+
name: field.id,
|
|
1265
|
+
value,
|
|
1266
|
+
onChange: (e) => onChange(e.target.value),
|
|
1267
|
+
onBlur,
|
|
1268
|
+
required: field.required,
|
|
1269
|
+
min: field.min ? String(field.min) : void 0,
|
|
1270
|
+
max: field.max ? String(field.max) : void 0,
|
|
1271
|
+
"aria-invalid": error ? "true" : void 0,
|
|
1272
|
+
className: inputClass
|
|
1273
|
+
}
|
|
1274
|
+
);
|
|
1275
|
+
case "time":
|
|
1276
|
+
return /* @__PURE__ */ jsx8(
|
|
1277
|
+
"input",
|
|
1278
|
+
{
|
|
1279
|
+
type: "time",
|
|
1280
|
+
id: field.id,
|
|
1281
|
+
name: field.id,
|
|
1282
|
+
value,
|
|
1283
|
+
onChange: (e) => onChange(e.target.value),
|
|
1284
|
+
onBlur,
|
|
1285
|
+
required: field.required,
|
|
1286
|
+
"aria-invalid": error ? "true" : void 0,
|
|
1287
|
+
className: inputClass
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
// text, email, tel, url
|
|
1291
|
+
default:
|
|
1292
|
+
return /* @__PURE__ */ jsx8(
|
|
1293
|
+
"input",
|
|
1294
|
+
{
|
|
1295
|
+
type: field.type,
|
|
1296
|
+
id: field.id,
|
|
1297
|
+
name: field.id,
|
|
1298
|
+
value,
|
|
1299
|
+
onChange: (e) => onChange(e.target.value),
|
|
1300
|
+
onBlur,
|
|
1301
|
+
placeholder: field.placeholder,
|
|
1302
|
+
required: field.required,
|
|
1303
|
+
minLength: field.minLength,
|
|
1304
|
+
maxLength: field.maxLength,
|
|
1305
|
+
pattern: field.pattern,
|
|
1306
|
+
"aria-invalid": error ? "true" : void 0,
|
|
1307
|
+
className: inputClass
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ../blocks/src/system/runtime/hooks/useBookingSteps.ts
|
|
1314
|
+
function useBookingSteps(siteId, formConfig, services) {
|
|
1315
|
+
return useMemo3(() => {
|
|
1316
|
+
if (!formConfig) return [];
|
|
1317
|
+
const stepsArray = [];
|
|
1318
|
+
const effectiveServiceId = formConfig.settings?.serviceId || (services.length === 1 ? services[0].id : void 0);
|
|
1319
|
+
const needsServiceSelection = !formConfig.settings?.serviceId && services.length > 1 || formConfig.settings?.serviceIds && formConfig.settings.serviceIds.length > 1;
|
|
1320
|
+
if (needsServiceSelection) {
|
|
1321
|
+
stepsArray.push({
|
|
1322
|
+
id: "service-selection",
|
|
1323
|
+
title: "Select Service",
|
|
1324
|
+
component: React4.createElement(ServiceSelectionStep, { siteId, services }),
|
|
1325
|
+
condition: () => needsServiceSelection,
|
|
1326
|
+
validate: (data) => {
|
|
1327
|
+
if (!data.serviceId) {
|
|
1328
|
+
return { valid: false, errors: { _form: "Please select a service" } };
|
|
1329
|
+
}
|
|
1330
|
+
return { valid: true };
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
const singleService = services.length === 1 ? services[0] : void 0;
|
|
1335
|
+
stepsArray.push({
|
|
1336
|
+
id: "datetime-selection",
|
|
1337
|
+
title: "Select Date & Time",
|
|
1338
|
+
component: React4.createElement(DateTimeSelectionStep, {
|
|
1339
|
+
siteId,
|
|
1340
|
+
preSelectedServiceId: effectiveServiceId,
|
|
1341
|
+
preSelectedResourceId: formConfig.settings?.resourceId,
|
|
1342
|
+
singleService
|
|
1343
|
+
}),
|
|
1344
|
+
validate: (data) => {
|
|
1345
|
+
if (!data.selectedSlot) {
|
|
1346
|
+
return { valid: false, errors: { _form: "Please select a date and time" } };
|
|
1347
|
+
}
|
|
1348
|
+
return { valid: true };
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
if (formConfig.schema?.fields && formConfig.schema.fields.length > 0) {
|
|
1352
|
+
stepsArray.push({
|
|
1353
|
+
id: "custom-fields",
|
|
1354
|
+
title: "Your Information",
|
|
1355
|
+
component: React4.createElement(DynamicFormFields, { fields: formConfig.schema.fields }),
|
|
1356
|
+
validate: (data) => {
|
|
1357
|
+
const errors = {};
|
|
1358
|
+
for (const field of formConfig.schema.fields) {
|
|
1359
|
+
if (field.required && !data[field.id]) {
|
|
1360
|
+
errors[field.id] = `${field.label} is required`;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (Object.keys(errors).length > 0) {
|
|
1364
|
+
return { valid: false, errors };
|
|
1365
|
+
}
|
|
1366
|
+
return { valid: true };
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
return stepsArray;
|
|
1371
|
+
}, [formConfig, services, siteId]);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// ../blocks/src/system/runtime/hooks/useBookingSubmission.ts
|
|
1375
|
+
import { useState as useState6 } from "react";
|
|
1376
|
+
function useBookingSubmission(siteId) {
|
|
1377
|
+
const [isSubmitting, setIsSubmitting] = useState6(false);
|
|
1378
|
+
const [error, setError] = useState6(null);
|
|
1379
|
+
const [isSuccess, setIsSuccess] = useState6(false);
|
|
1380
|
+
const submit = async (data) => {
|
|
1381
|
+
setIsSubmitting(true);
|
|
1382
|
+
setError(null);
|
|
1383
|
+
try {
|
|
1384
|
+
const apiUrl = getCmsApiUrl();
|
|
1385
|
+
const response = await fetch(`${apiUrl}/public/bookings/appointments`, {
|
|
1386
|
+
method: "POST",
|
|
1387
|
+
headers: {
|
|
1388
|
+
"Content-Type": "application/json"
|
|
1389
|
+
},
|
|
1390
|
+
body: JSON.stringify({
|
|
1391
|
+
formId: data.formId,
|
|
1392
|
+
serviceId: data.serviceId,
|
|
1393
|
+
resourceId: data.resourceId || null,
|
|
1394
|
+
startAt: data.startAt,
|
|
1395
|
+
endAt: data.endAt,
|
|
1396
|
+
customFields: data.customFields,
|
|
1397
|
+
timezone: data.timezone
|
|
1398
|
+
})
|
|
1399
|
+
});
|
|
1400
|
+
if (!response.ok) {
|
|
1401
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1402
|
+
throw new Error(errorData.error || "Failed to book appointment");
|
|
1403
|
+
}
|
|
1404
|
+
setIsSuccess(true);
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
setError(err instanceof Error ? err.message : "Failed to book appointment");
|
|
1407
|
+
throw err;
|
|
1408
|
+
} finally {
|
|
1409
|
+
setIsSubmitting(false);
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
return { submit, isSubmitting, error, isSuccess };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// ../blocks/src/system/runtime/nodes/booking-form.client.tsx
|
|
1416
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1417
|
+
var BookingFormClient = ({
|
|
1418
|
+
siteId: siteIdProp,
|
|
1419
|
+
formId,
|
|
1420
|
+
className,
|
|
1421
|
+
form,
|
|
1422
|
+
services = []
|
|
1423
|
+
}) => {
|
|
1424
|
+
const [isSuccess, setIsSuccess] = useState7(false);
|
|
1425
|
+
const siteId = siteIdProp || form?.siteId || "";
|
|
1426
|
+
React5.useEffect(() => {
|
|
1427
|
+
if (!siteId) {
|
|
1428
|
+
console.error("[BookingFormClient] ERROR: siteId is missing!");
|
|
1429
|
+
}
|
|
1430
|
+
if (!form) {
|
|
1431
|
+
console.warn("[BookingFormClient] Form data not loaded");
|
|
1432
|
+
}
|
|
1433
|
+
console.log("[BookingFormClient] Loaded with:", {
|
|
1434
|
+
siteId,
|
|
1435
|
+
formId,
|
|
1436
|
+
hasForm: !!form,
|
|
1437
|
+
servicesCount: Array.isArray(services) ? services.length : "not an array",
|
|
1438
|
+
servicesData: services
|
|
1439
|
+
});
|
|
1440
|
+
}, [siteId, formId, form, services]);
|
|
1441
|
+
const formConfig = form ? {
|
|
1442
|
+
id: form.id,
|
|
1443
|
+
name: form.name,
|
|
1444
|
+
slug: "",
|
|
1445
|
+
// Not needed for booking form
|
|
1446
|
+
settings: form.settingsJson,
|
|
1447
|
+
schema: form.schemaJson
|
|
1448
|
+
} : null;
|
|
1449
|
+
const normalizedServices = React5.useMemo(() => {
|
|
1450
|
+
if (Array.isArray(services)) {
|
|
1451
|
+
return services;
|
|
1452
|
+
}
|
|
1453
|
+
if (services && typeof services === "object") {
|
|
1454
|
+
if ("services" in services) {
|
|
1455
|
+
return services.services || [];
|
|
1456
|
+
}
|
|
1457
|
+
return Object.values(services);
|
|
1458
|
+
}
|
|
1459
|
+
return [];
|
|
1460
|
+
}, [services]);
|
|
1461
|
+
const steps = useBookingSteps(siteId, formConfig, normalizedServices);
|
|
1462
|
+
const { submit } = useBookingSubmission(siteId);
|
|
1463
|
+
if (!form) {
|
|
1464
|
+
return /* @__PURE__ */ jsx9("div", { className: `rounded-lg border border-destructive bg-destructive/10 p-4 ${className ?? ""}`, children: /* @__PURE__ */ jsx9("p", { className: "text-sm text-destructive", children: "Booking form not found. Please check your configuration." }) });
|
|
1465
|
+
}
|
|
1466
|
+
const handleComplete = async (data) => {
|
|
1467
|
+
try {
|
|
1468
|
+
const serviceId = data.serviceId || formConfig?.settings?.serviceId;
|
|
1469
|
+
const resourceId = data.resourceId || formConfig?.settings?.resourceId;
|
|
1470
|
+
if (!serviceId || !data.selectedSlot) {
|
|
1471
|
+
throw new Error("Missing required booking information");
|
|
1472
|
+
}
|
|
1473
|
+
console.log("[BookingFormClient] Looking for service:", {
|
|
1474
|
+
serviceId,
|
|
1475
|
+
availableServices: normalizedServices
|
|
1476
|
+
});
|
|
1477
|
+
const selectedService = normalizedServices.find((s) => s.id === serviceId);
|
|
1478
|
+
console.log("[BookingFormClient] Selected service:", selectedService);
|
|
1479
|
+
if (!selectedService) {
|
|
1480
|
+
throw new Error(`Service not found: ${serviceId}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (!selectedService.durationMinutes) {
|
|
1483
|
+
throw new Error(
|
|
1484
|
+
`Service "${selectedService.title || serviceId}" is missing duration. Please update the service in the dashboard to include a duration (e.g., 30 minutes).`
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
const startDate = new Date(data.selectedSlot);
|
|
1488
|
+
const endDate = new Date(startDate.getTime() + selectedService.durationMinutes * 60 * 1e3);
|
|
1489
|
+
const bookingFields = /* @__PURE__ */ new Set(["serviceId", "resourceId", "selectedDate", "selectedSlot"]);
|
|
1490
|
+
const customFields = {};
|
|
1491
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1492
|
+
if (!bookingFields.has(key)) {
|
|
1493
|
+
customFields[key] = value;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
await submit({
|
|
1497
|
+
formId,
|
|
1498
|
+
serviceId,
|
|
1499
|
+
resourceId,
|
|
1500
|
+
startAt: data.selectedSlot,
|
|
1501
|
+
endAt: endDate.toISOString(),
|
|
1502
|
+
customFields,
|
|
1503
|
+
timezone: "UTC"
|
|
1504
|
+
// TODO: Get from resource or user preference
|
|
1505
|
+
});
|
|
1506
|
+
setIsSuccess(true);
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
console.error("Booking submission failed:", err);
|
|
1509
|
+
throw err;
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
if (isSuccess) {
|
|
1513
|
+
const successMessage = formConfig?.settings?.successMessage || "Your appointment has been booked! Check your email for confirmation.";
|
|
1514
|
+
return /* @__PURE__ */ jsx9(SuccessMessage, { message: successMessage, className });
|
|
1515
|
+
}
|
|
1516
|
+
return /* @__PURE__ */ jsx9("div", { className, children: /* @__PURE__ */ jsx9(
|
|
1517
|
+
MultiStepForm,
|
|
1518
|
+
{
|
|
1519
|
+
steps,
|
|
1520
|
+
initialData: {
|
|
1521
|
+
serviceId: formConfig?.settings?.serviceId,
|
|
1522
|
+
resourceId: formConfig?.settings?.resourceId
|
|
1523
|
+
},
|
|
1524
|
+
onComplete: handleComplete,
|
|
1525
|
+
progressStyle: "steps",
|
|
1526
|
+
persistToUrl: false,
|
|
1527
|
+
allowSkip: false
|
|
1528
|
+
}
|
|
1529
|
+
) });
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
// ../blocks/src/system/runtime/hooks/useBookingFormConfig.ts
|
|
1533
|
+
import { useState as useState8, useEffect as useEffect6 } from "react";
|
|
1534
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1535
|
+
function useBookingFormConfig(siteId, formId) {
|
|
1536
|
+
const [formConfig, setFormConfig] = useState8(null);
|
|
1537
|
+
const [services, setServices] = useState8([]);
|
|
1538
|
+
const [isLoading, setIsLoading] = useState8(true);
|
|
1539
|
+
const [error, setError] = useState8(null);
|
|
1540
|
+
useEffect6(() => {
|
|
1541
|
+
let mounted = true;
|
|
1542
|
+
async function loadFormAndServices() {
|
|
1543
|
+
try {
|
|
1544
|
+
setIsLoading(true);
|
|
1545
|
+
setError(null);
|
|
1546
|
+
const apiUrl = getCmsApiUrl();
|
|
1547
|
+
const isUUID = UUID_REGEX.test(formId);
|
|
1548
|
+
const formUrl = isUUID ? `${apiUrl}/public/forms/${formId}` : `${apiUrl}/public/forms/${formId}?siteId=${encodeURIComponent(siteId)}`;
|
|
1549
|
+
const response = await fetch(formUrl);
|
|
1550
|
+
if (!response.ok) {
|
|
1551
|
+
throw new Error("Failed to load booking form");
|
|
1552
|
+
}
|
|
1553
|
+
const { form: rawData } = await response.json();
|
|
1554
|
+
if (!mounted) return;
|
|
1555
|
+
const normalizedForm = {
|
|
1556
|
+
id: rawData.id,
|
|
1557
|
+
name: rawData.name,
|
|
1558
|
+
slug: rawData.slug,
|
|
1559
|
+
settings: rawData.settingsJson ?? rawData.settings,
|
|
1560
|
+
schema: rawData.schemaJson ?? rawData.schema
|
|
1561
|
+
};
|
|
1562
|
+
setFormConfig(normalizedForm);
|
|
1563
|
+
if (!normalizedForm.settings?.serviceId && !normalizedForm.settings?.serviceIds) {
|
|
1564
|
+
const servicesResponse = await fetch(
|
|
1565
|
+
`${apiUrl}/public/bookings/services?siteId=${encodeURIComponent(siteId)}`
|
|
1566
|
+
);
|
|
1567
|
+
if (servicesResponse.ok) {
|
|
1568
|
+
const servicesData = await servicesResponse.json();
|
|
1569
|
+
if (mounted) {
|
|
1570
|
+
setServices(servicesData.services || []);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
} else if (normalizedForm.settings?.serviceIds && normalizedForm.settings.serviceIds.length > 0) {
|
|
1574
|
+
const servicesResponse = await fetch(
|
|
1575
|
+
`${apiUrl}/public/bookings/services?siteId=${encodeURIComponent(siteId)}&ids=${normalizedForm.settings.serviceIds.join(",")}`
|
|
1576
|
+
);
|
|
1577
|
+
if (servicesResponse.ok) {
|
|
1578
|
+
const servicesData = await servicesResponse.json();
|
|
1579
|
+
if (mounted) {
|
|
1580
|
+
setServices(servicesData.services || []);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
} else if (normalizedForm.settings?.serviceId) {
|
|
1584
|
+
const serviceResponse = await fetch(
|
|
1585
|
+
`${apiUrl}/public/bookings/services/${normalizedForm.settings.serviceId}?siteId=${encodeURIComponent(siteId)}`
|
|
1586
|
+
);
|
|
1587
|
+
if (serviceResponse.ok) {
|
|
1588
|
+
const serviceData = await serviceResponse.json();
|
|
1589
|
+
if (mounted) {
|
|
1590
|
+
setServices([serviceData.service || serviceData]);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
console.error("Failed to load form:", err);
|
|
1596
|
+
if (mounted) {
|
|
1597
|
+
setError("Failed to load booking form. Please try again later.");
|
|
1598
|
+
}
|
|
1599
|
+
} finally {
|
|
1600
|
+
if (mounted) {
|
|
1601
|
+
setIsLoading(false);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
loadFormAndServices();
|
|
1606
|
+
return () => {
|
|
1607
|
+
mounted = false;
|
|
1608
|
+
};
|
|
1609
|
+
}, [siteId, formId]);
|
|
1610
|
+
return { formConfig, services, isLoading, error };
|
|
1611
|
+
}
|
|
7
1612
|
export {
|
|
8
1613
|
BookingFormClient,
|
|
9
1614
|
useBookingFormConfig
|