@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.
Files changed (176) hide show
  1. package/dist/cli/index.js +4840 -9
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/client/bookings.d.mts +82 -2
  4. package/dist/client/bookings.d.ts +82 -2
  5. package/dist/client/bookings.js +1623 -3
  6. package/dist/client/bookings.js.map +1 -1
  7. package/dist/client/bookings.mjs +1610 -5
  8. package/dist/client/bookings.mjs.map +1 -1
  9. package/dist/client/client.d.mts +8 -5
  10. package/dist/client/client.d.ts +8 -5
  11. package/dist/client/client.js +16856 -322
  12. package/dist/client/client.js.map +1 -1
  13. package/dist/client/client.mjs +16838 -307
  14. package/dist/client/client.mjs.map +1 -1
  15. package/dist/client/hooks.d.mts +10 -7
  16. package/dist/client/hooks.d.ts +10 -7
  17. package/dist/client/hooks.js +5074 -4
  18. package/dist/client/hooks.js.map +1 -1
  19. package/dist/client/hooks.mjs +5074 -4
  20. package/dist/client/hooks.mjs.map +1 -1
  21. package/dist/client/rendering/client.d.mts +7 -1
  22. package/dist/client/rendering/client.d.ts +7 -1
  23. package/dist/client/rendering/client.js +17388 -2
  24. package/dist/client/rendering/client.js.map +1 -1
  25. package/dist/client/rendering/client.mjs +17382 -2
  26. package/dist/client/rendering/client.mjs.map +1 -1
  27. package/dist/client/resolver-BhueZVxZ.d.mts +61 -0
  28. package/dist/client/resolver-BhueZVxZ.d.ts +61 -0
  29. package/dist/client/usePage-BBcFCxOU.d.ts +6297 -0
  30. package/dist/client/usePage-BydHcMYB.d.mts +6297 -0
  31. package/dist/server/Layout-CLg8oH_S.d.ts +44 -0
  32. package/dist/server/Layout-DK_9OOgb.d.mts +44 -0
  33. package/dist/server/chunk-3J46ILMJ.mjs +2111 -0
  34. package/dist/server/chunk-3J46ILMJ.mjs.map +1 -0
  35. package/dist/server/{chunk-JB4LIEFS.js → chunk-5R4NMVXA.js} +15 -8
  36. package/dist/server/chunk-5R4NMVXA.js.map +1 -0
  37. package/dist/server/{chunk-ADREPXFU.js → chunk-62ZJI564.js} +3 -3
  38. package/dist/server/{chunk-ADREPXFU.js.map → chunk-62ZJI564.js.map} +1 -1
  39. package/dist/server/chunk-7DS4Q3GA.mjs +333 -0
  40. package/dist/server/chunk-7DS4Q3GA.mjs.map +1 -0
  41. package/dist/server/chunk-BJTO5JO5.mjs +11 -0
  42. package/dist/server/{chunk-4Z5FBFRL.mjs → chunk-BPKYRPCQ.mjs} +7 -3
  43. package/dist/server/{chunk-4Z5FBFRL.mjs.map → chunk-BPKYRPCQ.mjs.map} +1 -1
  44. package/dist/server/chunk-DGUM43GV.js +11 -0
  45. package/dist/server/chunk-DGUM43GV.js.map +1 -0
  46. package/dist/server/chunk-EGTDJ4PL.js +5461 -0
  47. package/dist/server/chunk-EGTDJ4PL.js.map +1 -0
  48. package/dist/server/chunk-FK64TZBT.mjs +831 -0
  49. package/dist/server/chunk-FK64TZBT.mjs.map +1 -0
  50. package/dist/server/chunk-GKYNDDJS.js +2111 -0
  51. package/dist/server/chunk-GKYNDDJS.js.map +1 -0
  52. package/dist/server/chunk-HOY77YBF.js +333 -0
  53. package/dist/server/chunk-HOY77YBF.js.map +1 -0
  54. package/dist/server/chunk-INWKF3IC.js +831 -0
  55. package/dist/server/chunk-INWKF3IC.js.map +1 -0
  56. package/dist/server/{chunk-2RW5HAQQ.mjs → chunk-JTAERCX2.mjs} +2 -2
  57. package/dist/server/chunk-O5DC7MYW.mjs +9606 -0
  58. package/dist/server/chunk-O5DC7MYW.mjs.map +1 -0
  59. package/dist/server/{chunk-PEAXKTDU.mjs → chunk-OP2GHK27.mjs} +2 -2
  60. package/dist/server/{chunk-WKG57P2H.mjs → chunk-PN3CHDVX.mjs} +10 -3
  61. package/dist/server/{chunk-WKG57P2H.mjs.map → chunk-PN3CHDVX.mjs.map} +1 -1
  62. package/dist/server/chunk-SF63XAX7.js +9606 -0
  63. package/dist/server/chunk-SF63XAX7.js.map +1 -0
  64. package/dist/server/{chunk-F472SMKX.js → chunk-TO7FD6TQ.js} +4 -4
  65. package/dist/server/{chunk-F472SMKX.js.map → chunk-TO7FD6TQ.js.map} +1 -1
  66. package/dist/server/chunk-USQF2XTU.mjs +5461 -0
  67. package/dist/server/chunk-USQF2XTU.mjs.map +1 -0
  68. package/dist/server/{chunk-SW7LE4M3.js → chunk-XLVL5WPH.js} +12 -8
  69. package/dist/server/chunk-XLVL5WPH.js.map +1 -0
  70. package/dist/server/components-BzdA6NAc.d.mts +305 -0
  71. package/dist/server/components-DhIcstww.d.ts +305 -0
  72. package/dist/server/components.d.mts +13 -49
  73. package/dist/server/components.d.ts +13 -49
  74. package/dist/server/components.js +7 -4
  75. package/dist/server/components.js.map +1 -1
  76. package/dist/server/components.mjs +9 -6
  77. package/dist/server/components.mjs.map +1 -1
  78. package/dist/server/config-validation.d.mts +2 -2
  79. package/dist/server/config-validation.d.ts +2 -2
  80. package/dist/server/config-validation.js +6 -3
  81. package/dist/server/config-validation.js.map +1 -1
  82. package/dist/server/config-validation.mjs +5 -2
  83. package/dist/server/config.d.mts +3 -3
  84. package/dist/server/config.d.ts +3 -3
  85. package/dist/server/config.js +6 -3
  86. package/dist/server/config.js.map +1 -1
  87. package/dist/server/config.mjs +5 -2
  88. package/dist/server/config.mjs.map +1 -1
  89. package/dist/server/data.d.mts +9 -8
  90. package/dist/server/data.d.ts +9 -8
  91. package/dist/server/data.js +4 -2
  92. package/dist/server/data.js.map +1 -1
  93. package/dist/server/data.mjs +3 -1
  94. package/dist/server/{index-C6M0Wfjq.d.ts → index-BB28KAui.d.ts} +1 -1
  95. package/dist/server/{index-B0yI_V6Z.d.mts → index-C_FVup_o.d.mts} +1 -1
  96. package/dist/server/index.d.mts +1554 -5
  97. package/dist/server/index.d.ts +1554 -5
  98. package/dist/server/index.js +4 -4
  99. package/dist/server/index.js.map +1 -1
  100. package/dist/server/index.mjs +4 -4
  101. package/dist/server/index.mjs.map +1 -1
  102. package/dist/server/{loadContent-CJcbYF3J.d.ts → loadContent-AQOBf_gP.d.ts} +4 -4
  103. package/dist/server/{loadContent-zhlL4YSE.d.mts → loadContent-DBmprsB4.d.mts} +4 -4
  104. package/dist/server/loadPage-3ECPF426.js +11 -0
  105. package/dist/server/loadPage-3ECPF426.js.map +1 -0
  106. package/dist/server/{loadPage-CCf15nt8.d.mts → loadPage-BMg8PJxJ.d.ts} +146 -5
  107. package/dist/server/loadPage-LW273NYO.mjs +11 -0
  108. package/dist/server/loadPage-LW273NYO.mjs.map +1 -0
  109. package/dist/server/{loadPage-BYmVMk0V.d.ts → loadPage-pg4HimlK.d.mts} +146 -5
  110. package/dist/server/metadata.d.mts +9 -6
  111. package/dist/server/metadata.d.ts +9 -6
  112. package/dist/server/metadata.js +3 -1
  113. package/dist/server/metadata.js.map +1 -1
  114. package/dist/server/metadata.mjs +2 -0
  115. package/dist/server/metadata.mjs.map +1 -1
  116. package/dist/server/rendering/server.d.mts +9 -7
  117. package/dist/server/rendering/server.d.ts +9 -7
  118. package/dist/server/rendering/server.js +7 -4
  119. package/dist/server/rendering/server.js.map +1 -1
  120. package/dist/server/rendering/server.mjs +6 -3
  121. package/dist/server/rendering.d.mts +172 -9
  122. package/dist/server/rendering.d.ts +172 -9
  123. package/dist/server/rendering.js +12 -9
  124. package/dist/server/rendering.js.map +1 -1
  125. package/dist/server/rendering.mjs +14 -11
  126. package/dist/server/rendering.mjs.map +1 -1
  127. package/dist/server/routing.d.mts +9 -6
  128. package/dist/server/routing.d.ts +9 -6
  129. package/dist/server/routing.js +4 -2
  130. package/dist/server/routing.js.map +1 -1
  131. package/dist/server/routing.mjs +3 -1
  132. package/dist/server/routing.mjs.map +1 -1
  133. package/dist/server/schema-Bpy9N5ZI.d.mts +1870 -0
  134. package/dist/server/schema-Bpy9N5ZI.d.ts +1870 -0
  135. package/dist/server/server.d.mts +11 -8
  136. package/dist/server/server.d.ts +11 -8
  137. package/dist/server/server.js +7 -5
  138. package/dist/server/server.js.map +1 -1
  139. package/dist/server/server.mjs +6 -4
  140. package/dist/server/theme-bridge.js +13 -10
  141. package/dist/server/theme-bridge.js.map +1 -1
  142. package/dist/server/theme-bridge.mjs +10 -7
  143. package/dist/server/theme-bridge.mjs.map +1 -1
  144. package/dist/server/theme.js +3 -1
  145. package/dist/server/theme.js.map +1 -1
  146. package/dist/server/theme.mjs +2 -0
  147. package/dist/server/theme.mjs.map +1 -1
  148. package/dist/server/{types-BCeqWtI2.d.ts → types--u4GLCAY.d.ts} +1 -1
  149. package/dist/server/types-BprgZt-t.d.ts +4149 -0
  150. package/dist/server/types-C0G9IxWO.d.mts +4149 -0
  151. package/dist/server/{types-Bbo01M7P.d.mts → types-_nDnPHpv.d.mts} +27 -1
  152. package/dist/server/{types-Bbo01M7P.d.ts → types-_nDnPHpv.d.ts} +27 -1
  153. package/dist/server/{types-BCeqWtI2.d.mts → types-_zWJTgv0.d.mts} +1 -1
  154. package/package.json +6 -6
  155. package/dist/server/chunk-3KKZVGH4.mjs +0 -179
  156. package/dist/server/chunk-3KKZVGH4.mjs.map +0 -1
  157. package/dist/server/chunk-4Z3GPTCS.js +0 -179
  158. package/dist/server/chunk-4Z3GPTCS.js.map +0 -1
  159. package/dist/server/chunk-JB4LIEFS.js.map +0 -1
  160. package/dist/server/chunk-QQ6U4QX6.js +0 -120
  161. package/dist/server/chunk-QQ6U4QX6.js.map +0 -1
  162. package/dist/server/chunk-R5YGLRUG.mjs +0 -122
  163. package/dist/server/chunk-R5YGLRUG.mjs.map +0 -1
  164. package/dist/server/chunk-SW7LE4M3.js.map +0 -1
  165. package/dist/server/chunk-W3K7LVPS.mjs +0 -120
  166. package/dist/server/chunk-W3K7LVPS.mjs.map +0 -1
  167. package/dist/server/chunk-YHEZMVTS.js +0 -122
  168. package/dist/server/chunk-YHEZMVTS.js.map +0 -1
  169. package/dist/server/loadPage-DVH3DW6E.js +0 -9
  170. package/dist/server/loadPage-DVH3DW6E.js.map +0 -1
  171. package/dist/server/loadPage-PHQZ6XQZ.mjs +0 -9
  172. package/dist/server/types-C6gmRHLe.d.mts +0 -150
  173. package/dist/server/types-C6gmRHLe.d.ts +0 -150
  174. /package/dist/server/{loadPage-PHQZ6XQZ.mjs.map → chunk-BJTO5JO5.mjs.map} +0 -0
  175. /package/dist/server/{chunk-2RW5HAQQ.mjs.map → chunk-JTAERCX2.mjs.map} +0 -0
  176. /package/dist/server/{chunk-PEAXKTDU.mjs.map → chunk-OP2GHK27.mjs.map} +0 -0
@@ -1,9 +1,1614 @@
1
1
  "use client";
2
- // src/bookings/index.ts
3
- import {
4
- BookingFormClient,
5
- useBookingFormConfig
6
- } from "@riverbankcms/blocks/client";
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