@idkwebsites/components 0.1.5 → 0.1.8
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/index.cjs +792 -206
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -8
- package/dist/index.d.ts +78 -8
- package/dist/index.js +780 -194
- package/dist/index.js.map +1 -1
- package/dist/styles.css +275 -0
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -136,29 +136,97 @@ function useTenant() {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// src/core/hooks/useServices.ts
|
|
139
|
+
var import_react3 = require("react");
|
|
139
140
|
var import_react_query3 = require("@tanstack/react-query");
|
|
140
|
-
function
|
|
141
|
+
function buildQuery(params) {
|
|
142
|
+
if (!params) return "";
|
|
143
|
+
const searchParams = new URLSearchParams();
|
|
144
|
+
const categories = Array.isArray(params.category) ? params.category : params.category ? [params.category] : [];
|
|
145
|
+
if (categories.length) searchParams.set("category", categories.join(","));
|
|
146
|
+
if (params.staffId) searchParams.set("staffId", params.staffId);
|
|
147
|
+
if (typeof params.isActive === "boolean") searchParams.set("isActive", String(params.isActive));
|
|
148
|
+
if (typeof params.minPrice === "number") searchParams.set("minPrice", String(params.minPrice));
|
|
149
|
+
if (typeof params.maxPrice === "number") searchParams.set("maxPrice", String(params.maxPrice));
|
|
150
|
+
if (params.search) searchParams.set("search", params.search);
|
|
151
|
+
if (params.ids?.length) searchParams.set("ids", params.ids.join(","));
|
|
152
|
+
if (typeof params.limit === "number") searchParams.set("limit", String(params.limit));
|
|
153
|
+
if (params.sort) searchParams.set("sort", params.sort);
|
|
154
|
+
if (params.order) searchParams.set("order", params.order);
|
|
155
|
+
return searchParams.toString();
|
|
156
|
+
}
|
|
157
|
+
function useServices(params) {
|
|
141
158
|
const config = usePlatformConfig();
|
|
159
|
+
const queryString = (0, import_react3.useMemo)(() => buildQuery(params), [
|
|
160
|
+
params?.category,
|
|
161
|
+
params?.staffId,
|
|
162
|
+
params?.isActive,
|
|
163
|
+
params?.minPrice,
|
|
164
|
+
params?.maxPrice,
|
|
165
|
+
params?.search,
|
|
166
|
+
params?.ids,
|
|
167
|
+
params?.limit,
|
|
168
|
+
params?.sort,
|
|
169
|
+
params?.order
|
|
170
|
+
]);
|
|
171
|
+
const enabled = params?.enabled ?? true;
|
|
142
172
|
return (0, import_react_query3.useQuery)({
|
|
143
|
-
queryKey: ["idk", "services"],
|
|
144
|
-
queryFn: () => apiRequest(
|
|
173
|
+
queryKey: ["idk", "services", queryString],
|
|
174
|
+
queryFn: () => apiRequest(
|
|
175
|
+
config,
|
|
176
|
+
queryString ? `/services?${queryString}` : "/services"
|
|
177
|
+
),
|
|
178
|
+
enabled
|
|
145
179
|
});
|
|
146
180
|
}
|
|
147
181
|
|
|
148
182
|
// src/core/hooks/useTeam.ts
|
|
183
|
+
var import_react4 = require("react");
|
|
149
184
|
var import_react_query4 = require("@tanstack/react-query");
|
|
150
|
-
function
|
|
185
|
+
function buildQuery2(params) {
|
|
186
|
+
if (!params) return "";
|
|
187
|
+
const searchParams = new URLSearchParams();
|
|
188
|
+
const roles = Array.isArray(params.role) ? params.role : params.role ? [params.role] : [];
|
|
189
|
+
if (roles.length) searchParams.set("role", roles.join(","));
|
|
190
|
+
if (typeof params.isActive === "boolean") searchParams.set("isActive", String(params.isActive));
|
|
191
|
+
if (typeof params.acceptsNewBookings === "boolean") {
|
|
192
|
+
searchParams.set("acceptsNewBookings", String(params.acceptsNewBookings));
|
|
193
|
+
}
|
|
194
|
+
if (params.search) searchParams.set("search", params.search);
|
|
195
|
+
if (params.ids?.length) searchParams.set("ids", params.ids.join(","));
|
|
196
|
+
if (params.serviceId) searchParams.set("serviceId", params.serviceId);
|
|
197
|
+
if (typeof params.limit === "number") searchParams.set("limit", String(params.limit));
|
|
198
|
+
if (params.sort) searchParams.set("sort", params.sort);
|
|
199
|
+
if (params.order) searchParams.set("order", params.order);
|
|
200
|
+
return searchParams.toString();
|
|
201
|
+
}
|
|
202
|
+
function useTeam(params) {
|
|
151
203
|
const config = usePlatformConfig();
|
|
204
|
+
const queryString = (0, import_react4.useMemo)(() => buildQuery2(params), [
|
|
205
|
+
params?.role,
|
|
206
|
+
params?.isActive,
|
|
207
|
+
params?.acceptsNewBookings,
|
|
208
|
+
params?.search,
|
|
209
|
+
params?.ids,
|
|
210
|
+
params?.serviceId,
|
|
211
|
+
params?.limit,
|
|
212
|
+
params?.sort,
|
|
213
|
+
params?.order
|
|
214
|
+
]);
|
|
215
|
+
const enabled = params?.enabled ?? true;
|
|
152
216
|
return (0, import_react_query4.useQuery)({
|
|
153
|
-
queryKey: ["idk", "team"],
|
|
154
|
-
queryFn: () => apiRequest(
|
|
217
|
+
queryKey: ["idk", "team", queryString],
|
|
218
|
+
queryFn: () => apiRequest(
|
|
219
|
+
config,
|
|
220
|
+
queryString ? `/team?${queryString}` : "/team"
|
|
221
|
+
),
|
|
222
|
+
enabled
|
|
155
223
|
});
|
|
156
224
|
}
|
|
157
225
|
|
|
158
226
|
// src/core/hooks/useAvailability.ts
|
|
159
|
-
var
|
|
227
|
+
var import_react5 = require("react");
|
|
160
228
|
var import_react_query5 = require("@tanstack/react-query");
|
|
161
|
-
function
|
|
229
|
+
function buildQuery3(params) {
|
|
162
230
|
const searchParams = new URLSearchParams();
|
|
163
231
|
searchParams.set("serviceId", params.serviceId);
|
|
164
232
|
if (params.staffId) searchParams.set("staffId", params.staffId);
|
|
@@ -170,8 +238,8 @@ function buildQuery(params) {
|
|
|
170
238
|
}
|
|
171
239
|
function useAvailability(params) {
|
|
172
240
|
const config = usePlatformConfig();
|
|
173
|
-
const queryString = (0,
|
|
174
|
-
() =>
|
|
241
|
+
const queryString = (0, import_react5.useMemo)(
|
|
242
|
+
() => buildQuery3(params),
|
|
175
243
|
[params.serviceId, params.staffId, params.startDate, params.endDate, params.date, params.days]
|
|
176
244
|
);
|
|
177
245
|
const enabled = params.enabled ?? Boolean(params.serviceId);
|
|
@@ -221,6 +289,7 @@ function ServiceCard({
|
|
|
221
289
|
className,
|
|
222
290
|
showDescription = true,
|
|
223
291
|
showPrice = true,
|
|
292
|
+
showImage = false,
|
|
224
293
|
onSelect
|
|
225
294
|
}) {
|
|
226
295
|
const handleClick = () => {
|
|
@@ -234,6 +303,7 @@ function ServiceCard({
|
|
|
234
303
|
role: onSelect ? "button" : void 0,
|
|
235
304
|
tabIndex: onSelect ? 0 : void 0,
|
|
236
305
|
children: [
|
|
306
|
+
showImage && service.image?.url ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "idk-card__media", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: service.image.url, alt: service.name }) }) : null,
|
|
237
307
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "idk-card__header", children: [
|
|
238
308
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "idk-card__title", children: service.name }),
|
|
239
309
|
showPrice && typeof service.price === "number" && service.price > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "idk-card__price", children: [
|
|
@@ -262,14 +332,31 @@ function ServicesList({
|
|
|
262
332
|
className,
|
|
263
333
|
showDescription,
|
|
264
334
|
showPrice,
|
|
335
|
+
showImage,
|
|
336
|
+
query,
|
|
337
|
+
filter,
|
|
338
|
+
sort,
|
|
339
|
+
limit,
|
|
340
|
+
emptyMessage = "No services available.",
|
|
341
|
+
loadingMessage = "Loading services...",
|
|
265
342
|
onSelect
|
|
266
343
|
}) {
|
|
267
|
-
const { data, isLoading, error } = useServices();
|
|
344
|
+
const { data, isLoading, error } = useServices(query);
|
|
268
345
|
if (isLoading) {
|
|
269
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "idk-state", children:
|
|
346
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "idk-state", children: loadingMessage });
|
|
347
|
+
}
|
|
348
|
+
let services = data?.services ? [...data.services] : [];
|
|
349
|
+
if (filter) {
|
|
350
|
+
services = services.filter(filter);
|
|
351
|
+
}
|
|
352
|
+
if (sort) {
|
|
353
|
+
services = services.sort(sort);
|
|
270
354
|
}
|
|
271
|
-
if (
|
|
272
|
-
|
|
355
|
+
if (typeof limit === "number") {
|
|
356
|
+
services = services.slice(0, Math.max(0, limit));
|
|
357
|
+
}
|
|
358
|
+
if (error || services.length === 0) {
|
|
359
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "idk-state", children: emptyMessage });
|
|
273
360
|
}
|
|
274
361
|
const gridStyle = layout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))` } : void 0;
|
|
275
362
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -281,12 +368,13 @@ function ServicesList({
|
|
|
281
368
|
className
|
|
282
369
|
].filter(Boolean).join(" "),
|
|
283
370
|
style: gridStyle,
|
|
284
|
-
children:
|
|
371
|
+
children: services.map((service) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
285
372
|
ServiceCard,
|
|
286
373
|
{
|
|
287
374
|
service,
|
|
288
375
|
showDescription,
|
|
289
376
|
showPrice,
|
|
377
|
+
showImage,
|
|
290
378
|
onSelect
|
|
291
379
|
},
|
|
292
380
|
service.id
|
|
@@ -301,14 +389,31 @@ function TeamMember({
|
|
|
301
389
|
member,
|
|
302
390
|
className,
|
|
303
391
|
showRole = true,
|
|
304
|
-
showEmail = false
|
|
392
|
+
showEmail = false,
|
|
393
|
+
showBio = false,
|
|
394
|
+
layout = "card",
|
|
395
|
+
imagePosition = "left"
|
|
305
396
|
}) {
|
|
306
397
|
const initials = member.name?.split(" ").map((part) => part[0]).slice(0, 2).join("").toUpperCase();
|
|
398
|
+
const avatar = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "idk-team__avatar", children: member.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { src: member.photo.url, alt: member.name }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: initials }) });
|
|
399
|
+
if (layout === "profile") {
|
|
400
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: ["idk-team__profile", className].filter(Boolean).join(" "), children: [
|
|
401
|
+
imagePosition === "left" ? avatar : null,
|
|
402
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "idk-team__info", children: [
|
|
403
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "idk-card__title", children: member.name }),
|
|
404
|
+
showRole ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "idk-card__description", children: member.role || "Staff" }) : null,
|
|
405
|
+
showBio && member.bio ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "idk-team__bio", children: member.bio }) : null,
|
|
406
|
+
showEmail ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "idk-card__meta", children: member.email }) : null
|
|
407
|
+
] }),
|
|
408
|
+
imagePosition === "right" ? avatar : null
|
|
409
|
+
] });
|
|
410
|
+
}
|
|
307
411
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: ["idk-card", className].filter(Boolean).join(" "), children: [
|
|
308
412
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "idk-team__avatar", children: member.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { src: member.photo.url, alt: member.name }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: initials }) }),
|
|
309
413
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "idk-team__info", children: [
|
|
310
414
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "idk-card__title", children: member.name }),
|
|
311
415
|
showRole ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "idk-card__description", children: member.role || "Staff" }) : null,
|
|
416
|
+
showBio && member.bio ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "idk-team__bio", children: member.bio }) : null,
|
|
312
417
|
showEmail ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "idk-card__meta", children: member.email }) : null
|
|
313
418
|
] })
|
|
314
419
|
] });
|
|
@@ -320,14 +425,33 @@ function TeamGrid({
|
|
|
320
425
|
columns = 3,
|
|
321
426
|
className,
|
|
322
427
|
showRole,
|
|
323
|
-
showEmail
|
|
428
|
+
showEmail,
|
|
429
|
+
showBio,
|
|
430
|
+
layout = "card",
|
|
431
|
+
imagePosition = "left",
|
|
432
|
+
query,
|
|
433
|
+
filter,
|
|
434
|
+
sort,
|
|
435
|
+
limit,
|
|
436
|
+
emptyMessage = "No team members available.",
|
|
437
|
+
loadingMessage = "Loading team..."
|
|
324
438
|
}) {
|
|
325
|
-
const { data, isLoading, error } = useTeam();
|
|
439
|
+
const { data, isLoading, error } = useTeam(query);
|
|
326
440
|
if (isLoading) {
|
|
327
|
-
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "idk-state", children:
|
|
441
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "idk-state", children: loadingMessage });
|
|
328
442
|
}
|
|
329
|
-
|
|
330
|
-
|
|
443
|
+
let members = data?.team ? [...data.team] : [];
|
|
444
|
+
if (filter) {
|
|
445
|
+
members = members.filter(filter);
|
|
446
|
+
}
|
|
447
|
+
if (sort) {
|
|
448
|
+
members = members.sort(sort);
|
|
449
|
+
}
|
|
450
|
+
if (typeof limit === "number") {
|
|
451
|
+
members = members.slice(0, Math.max(0, limit));
|
|
452
|
+
}
|
|
453
|
+
if (error || members.length === 0) {
|
|
454
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "idk-state", children: emptyMessage });
|
|
331
455
|
}
|
|
332
456
|
const gridStyle = { gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))` };
|
|
333
457
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
@@ -335,12 +459,15 @@ function TeamGrid({
|
|
|
335
459
|
{
|
|
336
460
|
className: ["idk-team", className].filter(Boolean).join(" "),
|
|
337
461
|
style: gridStyle,
|
|
338
|
-
children:
|
|
462
|
+
children: members.map((member) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
339
463
|
TeamMember,
|
|
340
464
|
{
|
|
341
465
|
member,
|
|
342
466
|
showRole,
|
|
343
|
-
showEmail
|
|
467
|
+
showEmail,
|
|
468
|
+
showBio,
|
|
469
|
+
layout,
|
|
470
|
+
imagePosition
|
|
344
471
|
},
|
|
345
472
|
member.id
|
|
346
473
|
))
|
|
@@ -349,7 +476,7 @@ function TeamGrid({
|
|
|
349
476
|
}
|
|
350
477
|
|
|
351
478
|
// src/components/ContactForm.tsx
|
|
352
|
-
var
|
|
479
|
+
var import_react6 = require("react");
|
|
353
480
|
var import_react_query7 = require("@tanstack/react-query");
|
|
354
481
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
355
482
|
var DEFAULT_FIELDS = ["name", "email", "phone", "message"];
|
|
@@ -362,7 +489,7 @@ function ContactForm({
|
|
|
362
489
|
onError
|
|
363
490
|
}) {
|
|
364
491
|
const config = usePlatformConfig();
|
|
365
|
-
const [formState, setFormState] = (0,
|
|
492
|
+
const [formState, setFormState] = (0, import_react6.useState)({
|
|
366
493
|
name: "",
|
|
367
494
|
email: "",
|
|
368
495
|
phone: "",
|
|
@@ -460,219 +587,678 @@ function ContactForm({
|
|
|
460
587
|
}
|
|
461
588
|
|
|
462
589
|
// src/components/BookingWidget.tsx
|
|
463
|
-
var
|
|
590
|
+
var import_react7 = require("react");
|
|
464
591
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
465
592
|
function BookingWidget({
|
|
466
593
|
className,
|
|
467
594
|
showStaffSelection = true,
|
|
468
|
-
onSuccess
|
|
595
|
+
onSuccess,
|
|
596
|
+
services,
|
|
597
|
+
servicesQuery,
|
|
598
|
+
teamQuery,
|
|
599
|
+
serviceFilter,
|
|
600
|
+
teamFilter,
|
|
601
|
+
title = "Book an Appointment",
|
|
602
|
+
subtitle,
|
|
603
|
+
autoAdvanceOnSelect = false,
|
|
604
|
+
scrollToStep = true,
|
|
605
|
+
scrollOffset = 96,
|
|
606
|
+
enableServiceSearch = true,
|
|
607
|
+
serviceSearchPlaceholder = "Search services",
|
|
608
|
+
enableCategoryFilter = false,
|
|
609
|
+
categoryLabel = "All categories",
|
|
610
|
+
serviceListMaxHeight,
|
|
611
|
+
serviceLayout = "list",
|
|
612
|
+
serviceColumns = 2,
|
|
613
|
+
servicePageSize = 10,
|
|
614
|
+
showServicePagination,
|
|
615
|
+
staffLayout = "grid",
|
|
616
|
+
staffColumns = 2,
|
|
617
|
+
showAnyStaffOption = true,
|
|
618
|
+
timeLayout = "split"
|
|
469
619
|
}) {
|
|
470
|
-
const
|
|
471
|
-
|
|
620
|
+
const resolvedServicesQuery = (0, import_react7.useMemo)(
|
|
621
|
+
() => services ? { ...servicesQuery, enabled: false } : servicesQuery,
|
|
622
|
+
[services, servicesQuery]
|
|
623
|
+
);
|
|
624
|
+
const { data: servicesData, isLoading: servicesLoading } = useServices(resolvedServicesQuery);
|
|
625
|
+
const { data: teamData } = useTeam(teamQuery);
|
|
472
626
|
const createBooking = useCreateBooking();
|
|
473
|
-
const
|
|
474
|
-
const [
|
|
475
|
-
const [
|
|
476
|
-
const [
|
|
477
|
-
const [
|
|
478
|
-
const [
|
|
479
|
-
const [
|
|
627
|
+
const widgetRef = (0, import_react7.useRef)(null);
|
|
628
|
+
const [step, setStep] = (0, import_react7.useState)("service");
|
|
629
|
+
const [selectedService, setSelectedService] = (0, import_react7.useState)(null);
|
|
630
|
+
const [selectedStaff, setSelectedStaff] = (0, import_react7.useState)(null);
|
|
631
|
+
const [selectedDate, setSelectedDate] = (0, import_react7.useState)(null);
|
|
632
|
+
const [selectedTime, setSelectedTime] = (0, import_react7.useState)(null);
|
|
633
|
+
const [selectedEndTime, setSelectedEndTime] = (0, import_react7.useState)(null);
|
|
634
|
+
const [serviceSearch, setServiceSearch] = (0, import_react7.useState)("");
|
|
635
|
+
const [categoryFilter, setCategoryFilter] = (0, import_react7.useState)("all");
|
|
636
|
+
const [servicePage, setServicePage] = (0, import_react7.useState)(1);
|
|
637
|
+
const [bookingError, setBookingError] = (0, import_react7.useState)(null);
|
|
638
|
+
const [details, setDetails] = (0, import_react7.useState)({
|
|
480
639
|
name: "",
|
|
481
640
|
email: "",
|
|
482
641
|
phone: "",
|
|
483
642
|
notes: ""
|
|
484
643
|
});
|
|
485
|
-
const
|
|
644
|
+
const servicesList = (0, import_react7.useMemo)(() => {
|
|
645
|
+
const list = services ? [...services] : servicesData?.services ? [...servicesData.services] : [];
|
|
646
|
+
return serviceFilter ? list.filter(serviceFilter) : list;
|
|
647
|
+
}, [services, servicesData, serviceFilter]);
|
|
648
|
+
const categories = (0, import_react7.useMemo)(() => {
|
|
649
|
+
const set = /* @__PURE__ */ new Set();
|
|
650
|
+
servicesList.forEach((service) => {
|
|
651
|
+
if (service.category) {
|
|
652
|
+
set.add(service.category);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
return Array.from(set).sort();
|
|
656
|
+
}, [servicesList]);
|
|
657
|
+
const filteredServices = (0, import_react7.useMemo)(() => {
|
|
658
|
+
const search = serviceSearch.trim().toLowerCase();
|
|
659
|
+
return servicesList.filter((service) => {
|
|
660
|
+
const matchesSearch = !search || service.name.toLowerCase().includes(search) || (service.description || "").toLowerCase().includes(search) || (service.category || "").toLowerCase().includes(search);
|
|
661
|
+
const matchesCategory = categoryFilter === "all" ? true : service.category === categoryFilter;
|
|
662
|
+
return matchesSearch && matchesCategory;
|
|
663
|
+
});
|
|
664
|
+
}, [servicesList, serviceSearch, categoryFilter]);
|
|
665
|
+
(0, import_react7.useEffect)(() => {
|
|
666
|
+
setServicePage(1);
|
|
667
|
+
}, [serviceSearch, categoryFilter]);
|
|
668
|
+
const pagedServices = (0, import_react7.useMemo)(() => {
|
|
669
|
+
const pageSize = Math.max(1, servicePageSize);
|
|
670
|
+
return filteredServices.slice(0, pageSize * servicePage);
|
|
671
|
+
}, [filteredServices, servicePage, servicePageSize]);
|
|
672
|
+
const hasMoreServices = filteredServices.length > pagedServices.length;
|
|
673
|
+
const shouldPaginate = showServicePagination ?? hasMoreServices;
|
|
674
|
+
const staffOptions = (0, import_react7.useMemo)(() => {
|
|
486
675
|
if (!selectedService) return [];
|
|
487
676
|
if (selectedService.assignedStaff && selectedService.assignedStaff.length > 0) {
|
|
488
|
-
|
|
677
|
+
const assigned = selectedService.assignedStaff;
|
|
678
|
+
return teamFilter ? assigned.filter(teamFilter) : assigned;
|
|
489
679
|
}
|
|
490
|
-
|
|
491
|
-
|
|
680
|
+
const teamList = teamData?.team || [];
|
|
681
|
+
return teamFilter ? teamList.filter(teamFilter) : teamList;
|
|
682
|
+
}, [selectedService, teamData, teamFilter]);
|
|
492
683
|
const availability = useAvailability({
|
|
493
684
|
serviceId: selectedService?.id || "",
|
|
494
685
|
staffId: selectedStaff?.id,
|
|
495
686
|
days: 7,
|
|
496
687
|
enabled: Boolean(selectedService)
|
|
497
688
|
});
|
|
689
|
+
const timeZone = availability.data?.timeZone || "America/Chicago";
|
|
690
|
+
const safeTimeZone = (0, import_react7.useMemo)(() => {
|
|
691
|
+
if (!timeZone) return void 0;
|
|
692
|
+
try {
|
|
693
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(/* @__PURE__ */ new Date());
|
|
694
|
+
return timeZone;
|
|
695
|
+
} catch {
|
|
696
|
+
return void 0;
|
|
697
|
+
}
|
|
698
|
+
}, [timeZone]);
|
|
699
|
+
const timeZoneLabel = (0, import_react7.useMemo)(() => {
|
|
700
|
+
if (timeZone) return timeZone;
|
|
701
|
+
try {
|
|
702
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
703
|
+
} catch {
|
|
704
|
+
return "Local time";
|
|
705
|
+
}
|
|
706
|
+
}, [timeZone]);
|
|
707
|
+
const dateLabelFormatter = (0, import_react7.useMemo)(
|
|
708
|
+
() => new Intl.DateTimeFormat("en-US", {
|
|
709
|
+
weekday: "short",
|
|
710
|
+
month: "short",
|
|
711
|
+
day: "numeric"
|
|
712
|
+
}),
|
|
713
|
+
[]
|
|
714
|
+
);
|
|
715
|
+
const dateHeadingFormatter = (0, import_react7.useMemo)(
|
|
716
|
+
() => new Intl.DateTimeFormat("en-US", {
|
|
717
|
+
weekday: "long",
|
|
718
|
+
month: "long",
|
|
719
|
+
day: "numeric"
|
|
720
|
+
}),
|
|
721
|
+
[]
|
|
722
|
+
);
|
|
723
|
+
const timeFormatter = (0, import_react7.useMemo)(
|
|
724
|
+
() => new Intl.DateTimeFormat("en-US", {
|
|
725
|
+
timeZone: safeTimeZone,
|
|
726
|
+
hour: "numeric",
|
|
727
|
+
minute: "2-digit"
|
|
728
|
+
}),
|
|
729
|
+
[safeTimeZone]
|
|
730
|
+
);
|
|
498
731
|
const requiresStaff = showStaffSelection || selectedService?.requiresStaffSelection || selectedService?.schedulingType === "customer-choice";
|
|
732
|
+
const canSkipStaff = !selectedService?.requiresStaffSelection;
|
|
733
|
+
const totalSteps = requiresStaff ? 4 : 3;
|
|
734
|
+
const stepNumber = (() => {
|
|
735
|
+
if (step === "service") return 1;
|
|
736
|
+
if (step === "staff") return 2;
|
|
737
|
+
if (step === "time") return requiresStaff ? 3 : 2;
|
|
738
|
+
if (step === "details") return requiresStaff ? 4 : 3;
|
|
739
|
+
return totalSteps;
|
|
740
|
+
})();
|
|
741
|
+
const scrollToTop = (0, import_react7.useCallback)(() => {
|
|
742
|
+
if (!scrollToStep || !widgetRef.current) return;
|
|
743
|
+
const top = widgetRef.current.getBoundingClientRect().top + window.scrollY;
|
|
744
|
+
window.scrollTo({
|
|
745
|
+
top: Math.max(0, top - scrollOffset),
|
|
746
|
+
behavior: "smooth"
|
|
747
|
+
});
|
|
748
|
+
}, [scrollToStep, scrollOffset]);
|
|
749
|
+
const resetSelections = () => {
|
|
750
|
+
setSelectedStaff(null);
|
|
751
|
+
setSelectedDate(null);
|
|
752
|
+
setSelectedTime(null);
|
|
753
|
+
setSelectedEndTime(null);
|
|
754
|
+
setBookingError(null);
|
|
755
|
+
};
|
|
756
|
+
(0, import_react7.useEffect)(() => {
|
|
757
|
+
if (step !== "time") return;
|
|
758
|
+
if (!availability.data?.dates?.length) return;
|
|
759
|
+
if (selectedDate) return;
|
|
760
|
+
setSelectedDate(availability.data.dates[0].date);
|
|
761
|
+
}, [step, availability.data, selectedDate]);
|
|
762
|
+
const parseDateOnly = (value) => {
|
|
763
|
+
if (!value) return null;
|
|
764
|
+
const normalized = value.includes("T") ? value.split("T")[0] : value;
|
|
765
|
+
const parsed = /* @__PURE__ */ new Date(`${normalized}T00:00:00`);
|
|
766
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
767
|
+
};
|
|
768
|
+
const parseDateTime = (value, fallbackDate) => {
|
|
769
|
+
if (!value) return null;
|
|
770
|
+
const direct = new Date(value);
|
|
771
|
+
if (!Number.isNaN(direct.getTime())) return direct;
|
|
772
|
+
if (fallbackDate) {
|
|
773
|
+
const normalized = fallbackDate.includes("T") ? fallbackDate.split("T")[0] : fallbackDate;
|
|
774
|
+
const combined = /* @__PURE__ */ new Date(`${normalized}T${value}`);
|
|
775
|
+
if (!Number.isNaN(combined.getTime())) return combined;
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
};
|
|
779
|
+
const formatDateLabel = (date) => {
|
|
780
|
+
const parsed = parseDateOnly(date);
|
|
781
|
+
if (!parsed) return date;
|
|
782
|
+
try {
|
|
783
|
+
return dateLabelFormatter.format(parsed);
|
|
784
|
+
} catch {
|
|
785
|
+
return date;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
const formatDateHeading = (date) => {
|
|
789
|
+
const parsed = parseDateOnly(date);
|
|
790
|
+
if (!parsed) return date;
|
|
791
|
+
try {
|
|
792
|
+
return dateHeadingFormatter.format(parsed);
|
|
793
|
+
} catch {
|
|
794
|
+
return date;
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const formatTimeLabel = (iso) => {
|
|
798
|
+
const parsed = parseDateTime(iso, selectedDate);
|
|
799
|
+
if (!parsed) return iso;
|
|
800
|
+
try {
|
|
801
|
+
return timeFormatter.format(parsed);
|
|
802
|
+
} catch {
|
|
803
|
+
return parsed.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
const formatDuration = (minutes) => {
|
|
807
|
+
if (!Number.isFinite(minutes)) return "";
|
|
808
|
+
if (minutes < 60) return `${minutes} min`;
|
|
809
|
+
const hours = Math.floor(minutes / 60);
|
|
810
|
+
const remaining = minutes % 60;
|
|
811
|
+
return remaining === 0 ? `${hours} hr${hours > 1 ? "s" : ""}` : `${hours} hr ${remaining} min`;
|
|
812
|
+
};
|
|
813
|
+
const handleBack = () => {
|
|
814
|
+
if (step === "service") return;
|
|
815
|
+
if (step === "staff") {
|
|
816
|
+
setStep("service");
|
|
817
|
+
} else if (step === "time") {
|
|
818
|
+
if (requiresStaff) {
|
|
819
|
+
setStep("staff");
|
|
820
|
+
} else {
|
|
821
|
+
setStep("service");
|
|
822
|
+
}
|
|
823
|
+
} else if (step === "details") {
|
|
824
|
+
setStep("time");
|
|
825
|
+
} else if (step === "done") {
|
|
826
|
+
setStep("details");
|
|
827
|
+
}
|
|
828
|
+
setBookingError(null);
|
|
829
|
+
scrollToTop();
|
|
830
|
+
};
|
|
831
|
+
const handleServiceContinue = () => {
|
|
832
|
+
if (!selectedService) return;
|
|
833
|
+
if (requiresStaff) {
|
|
834
|
+
setStep("staff");
|
|
835
|
+
} else {
|
|
836
|
+
setStep("time");
|
|
837
|
+
}
|
|
838
|
+
scrollToTop();
|
|
839
|
+
};
|
|
840
|
+
const handleStaffContinue = () => {
|
|
841
|
+
if (!canSkipStaff && !selectedStaff) return;
|
|
842
|
+
setStep("time");
|
|
843
|
+
scrollToTop();
|
|
844
|
+
};
|
|
845
|
+
const handleTimeContinue = () => {
|
|
846
|
+
if (!selectedTime || !selectedEndTime) return;
|
|
847
|
+
setStep("details");
|
|
848
|
+
scrollToTop();
|
|
849
|
+
};
|
|
499
850
|
const handleSubmit = async (event) => {
|
|
500
851
|
event.preventDefault();
|
|
501
852
|
if (!selectedService || !selectedDate || !selectedTime || !selectedEndTime) return;
|
|
502
|
-
const start =
|
|
503
|
-
const end =
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
853
|
+
const start = parseDateTime(selectedTime, selectedDate);
|
|
854
|
+
const end = parseDateTime(selectedEndTime, selectedDate);
|
|
855
|
+
if (!start || !end) {
|
|
856
|
+
setBookingError("Please select a valid time slot.");
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
setBookingError(null);
|
|
861
|
+
const result = await createBooking.mutateAsync({
|
|
862
|
+
serviceId: selectedService.id,
|
|
863
|
+
staffId: selectedStaff?.id,
|
|
864
|
+
startTime: start.toISOString(),
|
|
865
|
+
endTime: end.toISOString(),
|
|
866
|
+
customerName: details.name,
|
|
867
|
+
customerEmail: details.email,
|
|
868
|
+
customerPhone: details.phone || void 0,
|
|
869
|
+
customerNotes: details.notes || void 0
|
|
870
|
+
});
|
|
871
|
+
setStep("done");
|
|
872
|
+
onSuccess?.(result?.booking);
|
|
873
|
+
scrollToTop();
|
|
874
|
+
} catch (error) {
|
|
875
|
+
setBookingError(
|
|
876
|
+
error instanceof Error ? error.message : "Unable to complete booking. Please try again."
|
|
877
|
+
);
|
|
878
|
+
}
|
|
516
879
|
};
|
|
517
|
-
|
|
880
|
+
const isServicesLoading = services ? false : servicesLoading;
|
|
881
|
+
if (isServicesLoading) {
|
|
518
882
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "Loading booking options..." });
|
|
519
883
|
}
|
|
520
|
-
if (!
|
|
884
|
+
if (!servicesList.length) {
|
|
521
885
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "No services available." });
|
|
522
886
|
}
|
|
523
|
-
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
className: "idk-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
887
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
888
|
+
"div",
|
|
889
|
+
{
|
|
890
|
+
ref: widgetRef,
|
|
891
|
+
className: ["idk-booking", className].filter(Boolean).join(" "),
|
|
892
|
+
children: [
|
|
893
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__header", children: [
|
|
894
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__title-wrap", children: [
|
|
895
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h3", { className: "idk-booking__title", children: title }),
|
|
896
|
+
subtitle ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-booking__subtitle", children: subtitle }) : null
|
|
897
|
+
] }),
|
|
898
|
+
step !== "service" && step !== "done" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "idk-link", onClick: handleBack, children: "Back" }) : null
|
|
899
|
+
] }),
|
|
900
|
+
step !== "done" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__progress", children: [
|
|
901
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { children: [
|
|
902
|
+
"Step ",
|
|
903
|
+
stepNumber,
|
|
904
|
+
" of ",
|
|
905
|
+
totalSteps
|
|
906
|
+
] }),
|
|
907
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-booking__bar", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
908
|
+
"div",
|
|
909
|
+
{
|
|
910
|
+
className: "idk-booking__bar-fill",
|
|
911
|
+
style: { width: `${stepNumber / totalSteps * 100}%` }
|
|
537
912
|
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
913
|
+
) })
|
|
914
|
+
] }) : null,
|
|
915
|
+
step === "service" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__step", children: [
|
|
916
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h4", { children: "Select a service" }),
|
|
917
|
+
enableServiceSearch || enableCategoryFilter ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__filters", children: [
|
|
918
|
+
enableServiceSearch ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
919
|
+
"input",
|
|
920
|
+
{
|
|
921
|
+
type: "search",
|
|
922
|
+
value: serviceSearch,
|
|
923
|
+
onChange: (event) => setServiceSearch(event.target.value),
|
|
924
|
+
placeholder: serviceSearchPlaceholder
|
|
925
|
+
}
|
|
926
|
+
) : null,
|
|
927
|
+
enableCategoryFilter && categories.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
928
|
+
"select",
|
|
929
|
+
{
|
|
930
|
+
value: categoryFilter,
|
|
931
|
+
onChange: (event) => setCategoryFilter(event.target.value),
|
|
932
|
+
children: [
|
|
933
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "all", children: categoryLabel }),
|
|
934
|
+
categories.map((category) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: category, children: category }, category))
|
|
935
|
+
]
|
|
936
|
+
}
|
|
937
|
+
) : null
|
|
938
|
+
] }) : null,
|
|
939
|
+
filteredServices.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "No services match your filters." }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
940
|
+
"div",
|
|
941
|
+
{
|
|
942
|
+
className: [
|
|
943
|
+
"idk-services",
|
|
944
|
+
serviceLayout === "grid" ? "idk-services--grid" : "idk-services--list"
|
|
945
|
+
].join(" "),
|
|
946
|
+
style: {
|
|
947
|
+
...serviceLayout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, serviceColumns)}, minmax(0, 1fr))` } : void 0,
|
|
948
|
+
...serviceListMaxHeight ? { maxHeight: serviceListMaxHeight, overflow: "auto" } : void 0
|
|
949
|
+
},
|
|
950
|
+
children: pagedServices.map((service) => {
|
|
951
|
+
const isSelected = selectedService?.id === service.id;
|
|
952
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
953
|
+
"button",
|
|
954
|
+
{
|
|
955
|
+
type: "button",
|
|
956
|
+
className: ["idk-card", "idk-card--clickable", isSelected ? "is-active" : ""].join(" "),
|
|
957
|
+
onClick: () => {
|
|
958
|
+
setSelectedService(service);
|
|
959
|
+
resetSelections();
|
|
960
|
+
if (autoAdvanceOnSelect) {
|
|
961
|
+
if (requiresStaff) {
|
|
962
|
+
setStep("staff");
|
|
963
|
+
} else {
|
|
964
|
+
setStep("time");
|
|
965
|
+
}
|
|
966
|
+
scrollToTop();
|
|
967
|
+
}
|
|
968
|
+
},
|
|
969
|
+
children: [
|
|
970
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-card__header", children: [
|
|
971
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-card__title", children: service.name }),
|
|
972
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "idk-card__aside", children: [
|
|
973
|
+
isSelected ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-card__check", children: "\u2713" }) : null,
|
|
974
|
+
typeof service.price === "number" && service.price > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "idk-card__price", children: [
|
|
975
|
+
"$",
|
|
976
|
+
(service.price / 100).toFixed(2)
|
|
977
|
+
] }) : null
|
|
978
|
+
] })
|
|
979
|
+
] }),
|
|
980
|
+
service.description ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-card__description", children: service.description }) : null,
|
|
981
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-card__meta", children: [
|
|
982
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: formatDuration(service.duration) }),
|
|
983
|
+
service.category ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-pill", children: service.category }) : null
|
|
984
|
+
] })
|
|
985
|
+
]
|
|
986
|
+
},
|
|
987
|
+
service.id
|
|
988
|
+
);
|
|
989
|
+
})
|
|
990
|
+
}
|
|
991
|
+
),
|
|
992
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__actions", children: [
|
|
993
|
+
shouldPaginate ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
994
|
+
"button",
|
|
995
|
+
{
|
|
996
|
+
type: "button",
|
|
997
|
+
className: "idk-button idk-button--ghost",
|
|
998
|
+
onClick: () => setServicePage((prev) => prev + 1),
|
|
999
|
+
disabled: !hasMoreServices,
|
|
1000
|
+
children: hasMoreServices ? "Show more services" : "All services shown"
|
|
1001
|
+
}
|
|
1002
|
+
) : null,
|
|
1003
|
+
selectedService && !autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "idk-button", onClick: handleServiceContinue, children: "Continue" }) : null
|
|
1004
|
+
] })
|
|
1005
|
+
] }),
|
|
1006
|
+
step === "staff" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__step", children: [
|
|
1007
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h4", { children: "Select a team member" }),
|
|
1008
|
+
staffOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "No team members available." }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1009
|
+
"div",
|
|
1010
|
+
{
|
|
1011
|
+
className: [
|
|
1012
|
+
"idk-team",
|
|
1013
|
+
staffLayout === "grid" ? "idk-team--grid" : "idk-team--list"
|
|
1014
|
+
].join(" "),
|
|
1015
|
+
style: staffLayout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, staffColumns)}, minmax(0, 1fr))` } : void 0,
|
|
1016
|
+
children: [
|
|
1017
|
+
showAnyStaffOption && canSkipStaff ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1018
|
+
"button",
|
|
1019
|
+
{
|
|
1020
|
+
type: "button",
|
|
1021
|
+
className: ["idk-card", "idk-card--clickable", !selectedStaff ? "is-active" : ""].join(" "),
|
|
1022
|
+
onClick: () => {
|
|
1023
|
+
setSelectedStaff(null);
|
|
1024
|
+
setSelectedDate(null);
|
|
1025
|
+
setSelectedTime(null);
|
|
1026
|
+
setSelectedEndTime(null);
|
|
1027
|
+
if (autoAdvanceOnSelect) {
|
|
1028
|
+
setStep("time");
|
|
1029
|
+
scrollToTop();
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
children: [
|
|
1033
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-card__header", children: [
|
|
1034
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-card__title", children: "Any Available" }),
|
|
1035
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-card__badge", children: "Recommended" })
|
|
1036
|
+
] }),
|
|
1037
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-card__description", children: "We'll match you with the first available stylist." })
|
|
1038
|
+
]
|
|
1039
|
+
},
|
|
1040
|
+
"any-staff"
|
|
1041
|
+
) : null,
|
|
1042
|
+
staffOptions.map((staff) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1043
|
+
"button",
|
|
1044
|
+
{
|
|
1045
|
+
type: "button",
|
|
1046
|
+
className: ["idk-card", "idk-card--clickable", selectedStaff?.id === staff.id ? "is-active" : ""].join(" "),
|
|
1047
|
+
onClick: () => {
|
|
1048
|
+
setSelectedStaff(staff);
|
|
1049
|
+
setSelectedDate(null);
|
|
1050
|
+
setSelectedTime(null);
|
|
1051
|
+
setSelectedEndTime(null);
|
|
1052
|
+
setBookingError(null);
|
|
1053
|
+
if (autoAdvanceOnSelect) {
|
|
1054
|
+
setStep("time");
|
|
1055
|
+
scrollToTop();
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-team__card", children: [
|
|
1059
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-team__avatar", children: staff.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("img", { src: staff.photo.url, alt: staff.name }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: staff.name.split(" ").map((part) => part[0]).slice(0, 2).join("").toUpperCase() }) }),
|
|
1060
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
1061
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-card__title", children: staff.name }),
|
|
1062
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-card__meta", children: staff.role || "Staff" }),
|
|
1063
|
+
staff.bio ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-card__description", children: staff.bio }) : null
|
|
1064
|
+
] })
|
|
1065
|
+
] })
|
|
1066
|
+
},
|
|
1067
|
+
staff.id
|
|
1068
|
+
))
|
|
1069
|
+
]
|
|
1070
|
+
}
|
|
1071
|
+
),
|
|
1072
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__actions", children: [
|
|
1073
|
+
canSkipStaff ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1074
|
+
"button",
|
|
1075
|
+
{
|
|
1076
|
+
type: "button",
|
|
1077
|
+
className: "idk-link",
|
|
1078
|
+
onClick: () => {
|
|
1079
|
+
setSelectedStaff(null);
|
|
1080
|
+
setStep("time");
|
|
1081
|
+
scrollToTop();
|
|
1082
|
+
},
|
|
1083
|
+
children: "Continue without selecting"
|
|
1084
|
+
}
|
|
1085
|
+
) : null,
|
|
1086
|
+
!autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1087
|
+
"button",
|
|
1088
|
+
{
|
|
1089
|
+
type: "button",
|
|
1090
|
+
className: "idk-button",
|
|
1091
|
+
onClick: handleStaffContinue,
|
|
1092
|
+
disabled: !canSkipStaff && !selectedStaff,
|
|
1093
|
+
children: "Continue"
|
|
1094
|
+
}
|
|
1095
|
+
) : null
|
|
1096
|
+
] })
|
|
1097
|
+
] }),
|
|
1098
|
+
step === "time" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__step", children: [
|
|
1099
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h4", { children: "Select a time" }),
|
|
1100
|
+
availability.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "Loading availability..." }) : availability.data ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: ["idk-availability", timeLayout === "split" ? "idk-availability--split" : ""].join(" "), children: [
|
|
1101
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-availability__dates", children: availability.data.dates.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1102
|
+
"button",
|
|
1103
|
+
{
|
|
1104
|
+
type: "button",
|
|
1105
|
+
className: entry.date === selectedDate ? "is-active" : void 0,
|
|
1106
|
+
onClick: () => {
|
|
1107
|
+
setSelectedDate(entry.date);
|
|
1108
|
+
setSelectedTime(null);
|
|
1109
|
+
setSelectedEndTime(null);
|
|
1110
|
+
setBookingError(null);
|
|
1111
|
+
},
|
|
1112
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-date-chip__day", children: formatDateLabel(entry.date) })
|
|
1113
|
+
},
|
|
1114
|
+
entry.date
|
|
1115
|
+
)) }),
|
|
1116
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-availability__panel", children: [
|
|
1117
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-availability__panel-header", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
1118
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-availability__panel-title", children: selectedDate ? formatDateHeading(selectedDate) : availability.data.dates[0]?.date ? formatDateHeading(availability.data.dates[0].date) : "Select a date" }),
|
|
1119
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-availability__panel-subtitle", children: timeZoneLabel })
|
|
1120
|
+
] }) }),
|
|
1121
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-availability__slots", children: (() => {
|
|
1122
|
+
const activeEntry = availability.data.dates.find((entry) => entry.date === selectedDate) || availability.data.dates[0];
|
|
1123
|
+
if (!activeEntry) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "Select a date to see times." });
|
|
1124
|
+
const slots = (activeEntry.slots || []).filter((slot) => slot.available);
|
|
1125
|
+
if (slots.length === 0) {
|
|
1126
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "No available times for this date." });
|
|
1127
|
+
}
|
|
1128
|
+
return slots.map((slot) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1129
|
+
"button",
|
|
1130
|
+
{
|
|
1131
|
+
type: "button",
|
|
1132
|
+
className: slot.time === selectedTime ? "is-active" : void 0,
|
|
1133
|
+
onClick: () => {
|
|
1134
|
+
const date = selectedDate || activeEntry.date;
|
|
1135
|
+
setSelectedDate(date || null);
|
|
1136
|
+
setSelectedTime(slot.time);
|
|
1137
|
+
setSelectedEndTime(slot.endTime);
|
|
1138
|
+
setBookingError(null);
|
|
1139
|
+
if (autoAdvanceOnSelect) {
|
|
1140
|
+
setStep("details");
|
|
1141
|
+
scrollToTop();
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
children: formatTimeLabel(slot.time)
|
|
1145
|
+
},
|
|
1146
|
+
`${slot.time}-${slot.endTime}`
|
|
1147
|
+
));
|
|
1148
|
+
})() })
|
|
550
1149
|
] })
|
|
551
|
-
]
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
"
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
},
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
},
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.
|
|
638
|
-
"
|
|
639
|
-
{
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
)
|
|
656
|
-
]
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
660
|
-
"textarea",
|
|
661
|
-
{
|
|
662
|
-
rows: 4,
|
|
663
|
-
value: details.notes,
|
|
664
|
-
onChange: (event) => setDetails((prev) => ({ ...prev, notes: event.target.value }))
|
|
665
|
-
}
|
|
666
|
-
)
|
|
667
|
-
] }),
|
|
668
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "submit", className: "idk-button", disabled: createBooking.isPending, children: createBooking.isPending ? "Booking..." : "Confirm booking" })
|
|
669
|
-
] }),
|
|
670
|
-
step === "done" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "Booking confirmed. You'll receive a confirmation email shortly." })
|
|
671
|
-
] });
|
|
1150
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-state", children: "No availability found." }),
|
|
1151
|
+
!autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-booking__actions", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1152
|
+
"button",
|
|
1153
|
+
{
|
|
1154
|
+
type: "button",
|
|
1155
|
+
className: "idk-button",
|
|
1156
|
+
onClick: handleTimeContinue,
|
|
1157
|
+
disabled: !selectedTime || !selectedEndTime,
|
|
1158
|
+
children: "Continue"
|
|
1159
|
+
}
|
|
1160
|
+
) }) : null
|
|
1161
|
+
] }),
|
|
1162
|
+
step === "details" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("form", { className: "idk-form", onSubmit: handleSubmit, children: [
|
|
1163
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h4", { children: "Your details" }),
|
|
1164
|
+
bookingError ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-booking__error", children: bookingError }) : null,
|
|
1165
|
+
selectedService && selectedTime ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__summary", children: [
|
|
1166
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
1167
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-booking__summary-label", children: "Service" }),
|
|
1168
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: selectedService.name })
|
|
1169
|
+
] }),
|
|
1170
|
+
selectedStaff ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
1171
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-booking__summary-label", children: "Staff" }),
|
|
1172
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: selectedStaff.name })
|
|
1173
|
+
] }) : null,
|
|
1174
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
1175
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-booking__summary-label", children: "When" }),
|
|
1176
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
|
|
1177
|
+
formatDateHeading(selectedDate || ""),
|
|
1178
|
+
" \xB7 ",
|
|
1179
|
+
formatTimeLabel(selectedTime)
|
|
1180
|
+
] })
|
|
1181
|
+
] }),
|
|
1182
|
+
selectedService.price ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
1183
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "idk-booking__summary-label", children: "Price" }),
|
|
1184
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
|
|
1185
|
+
"$",
|
|
1186
|
+
(selectedService.price / 100).toFixed(2)
|
|
1187
|
+
] })
|
|
1188
|
+
] }) : null
|
|
1189
|
+
] }) : null,
|
|
1190
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
|
|
1191
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Name" }),
|
|
1192
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1193
|
+
"input",
|
|
1194
|
+
{
|
|
1195
|
+
value: details.name,
|
|
1196
|
+
onChange: (event) => setDetails((prev) => ({ ...prev, name: event.target.value })),
|
|
1197
|
+
required: true
|
|
1198
|
+
}
|
|
1199
|
+
)
|
|
1200
|
+
] }),
|
|
1201
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
|
|
1202
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Email" }),
|
|
1203
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1204
|
+
"input",
|
|
1205
|
+
{
|
|
1206
|
+
type: "email",
|
|
1207
|
+
value: details.email,
|
|
1208
|
+
onChange: (event) => setDetails((prev) => ({ ...prev, email: event.target.value })),
|
|
1209
|
+
required: true
|
|
1210
|
+
}
|
|
1211
|
+
)
|
|
1212
|
+
] }),
|
|
1213
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
|
|
1214
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Phone" }),
|
|
1215
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1216
|
+
"input",
|
|
1217
|
+
{
|
|
1218
|
+
value: details.phone,
|
|
1219
|
+
onChange: (event) => setDetails((prev) => ({ ...prev, phone: event.target.value }))
|
|
1220
|
+
}
|
|
1221
|
+
)
|
|
1222
|
+
] }),
|
|
1223
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
|
|
1224
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Notes" }),
|
|
1225
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1226
|
+
"textarea",
|
|
1227
|
+
{
|
|
1228
|
+
rows: 4,
|
|
1229
|
+
value: details.notes,
|
|
1230
|
+
onChange: (event) => setDetails((prev) => ({ ...prev, notes: event.target.value }))
|
|
1231
|
+
}
|
|
1232
|
+
)
|
|
1233
|
+
] }),
|
|
1234
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "submit", className: "idk-button", disabled: createBooking.isPending, children: createBooking.isPending ? "Booking..." : "Confirm booking" })
|
|
1235
|
+
] }),
|
|
1236
|
+
step === "done" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "idk-booking__done", children: [
|
|
1237
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "idk-booking__done-title", children: "Booking confirmed!" }),
|
|
1238
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-booking__done-text", children: "We'll send a confirmation email shortly with your appointment details." }),
|
|
1239
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1240
|
+
"button",
|
|
1241
|
+
{
|
|
1242
|
+
type: "button",
|
|
1243
|
+
className: "idk-link",
|
|
1244
|
+
onClick: () => {
|
|
1245
|
+
setStep("service");
|
|
1246
|
+
setSelectedService(null);
|
|
1247
|
+
resetSelections();
|
|
1248
|
+
setDetails({ name: "", email: "", phone: "", notes: "" });
|
|
1249
|
+
scrollToTop();
|
|
1250
|
+
},
|
|
1251
|
+
children: "Book another appointment"
|
|
1252
|
+
}
|
|
1253
|
+
)
|
|
1254
|
+
] })
|
|
1255
|
+
]
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
672
1258
|
}
|
|
673
1259
|
|
|
674
1260
|
// src/components/AvailabilityPicker.tsx
|
|
675
|
-
var
|
|
1261
|
+
var import_react8 = require("react");
|
|
676
1262
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
677
1263
|
function AvailabilityPicker({
|
|
678
1264
|
serviceId,
|
|
@@ -682,8 +1268,8 @@ function AvailabilityPicker({
|
|
|
682
1268
|
className,
|
|
683
1269
|
onSelect
|
|
684
1270
|
}) {
|
|
685
|
-
const [selectedDate, setSelectedDate] = (0,
|
|
686
|
-
const [selectedTime, setSelectedTime] = (0,
|
|
1271
|
+
const [selectedDate, setSelectedDate] = (0, import_react8.useState)(date || null);
|
|
1272
|
+
const [selectedTime, setSelectedTime] = (0, import_react8.useState)(null);
|
|
687
1273
|
const { data, isLoading } = useAvailability({
|
|
688
1274
|
serviceId,
|
|
689
1275
|
staffId,
|