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