@idkwebsites/components 0.1.16 → 0.1.17

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 CHANGED
@@ -35,6 +35,7 @@ __export(src_exports, {
35
35
  TimePicker: () => TimePicker,
36
36
  useAvailability: () => useAvailability,
37
37
  useBookingLookup: () => useBookingLookup,
38
+ useBookingWidget: () => useBookingWidget,
38
39
  useCancelBooking: () => useCancelBooking,
39
40
  useCreateBooking: () => useCreateBooking,
40
41
  useServices: () => useServices,
@@ -282,367 +283,8 @@ function useBookingLookup(uid, enabled = true) {
282
283
  });
283
284
  }
284
285
 
285
- // src/components/ServiceCard.tsx
286
- var import_jsx_runtime2 = require("react/jsx-runtime");
287
- function ServiceCard({
288
- service,
289
- className,
290
- showDescription = true,
291
- showPrice = true,
292
- showImage = false,
293
- onSelect
294
- }) {
295
- const handleClick = () => {
296
- if (onSelect) onSelect(service);
297
- };
298
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
299
- "div",
300
- {
301
- className: ["idk-card", onSelect ? "idk-card--clickable" : "", className].filter(Boolean).join(" "),
302
- onClick: onSelect ? handleClick : void 0,
303
- role: onSelect ? "button" : void 0,
304
- tabIndex: onSelect ? 0 : void 0,
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,
307
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "idk-card__header", children: [
308
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "idk-card__title", children: service.name }),
309
- showPrice && typeof service.price === "number" && service.price > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "idk-card__price", children: [
310
- "$",
311
- (service.price / 100).toFixed(2)
312
- ] }) : null
313
- ] }),
314
- showDescription && service.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "idk-card__description", children: service.description }) : null,
315
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "idk-card__meta", children: [
316
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
317
- service.duration,
318
- " min"
319
- ] }),
320
- service.category ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: service.category }) : null
321
- ] })
322
- ]
323
- }
324
- );
325
- }
326
-
327
- // src/components/ServicesList.tsx
328
- var import_jsx_runtime3 = require("react/jsx-runtime");
329
- function ServicesList({
330
- layout = "grid",
331
- columns = 3,
332
- className,
333
- showDescription,
334
- showPrice,
335
- showImage,
336
- query,
337
- filter,
338
- sort,
339
- limit,
340
- emptyMessage = "No services available.",
341
- loadingMessage = "Loading services...",
342
- onSelect
343
- }) {
344
- const { data, isLoading, error } = useServices(query);
345
- if (isLoading) {
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);
354
- }
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 });
360
- }
361
- const gridStyle = layout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))` } : void 0;
362
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
363
- "div",
364
- {
365
- className: [
366
- "idk-services",
367
- layout === "grid" ? "idk-services--grid" : "idk-services--list",
368
- className
369
- ].filter(Boolean).join(" "),
370
- style: gridStyle,
371
- children: services.map((service) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
372
- ServiceCard,
373
- {
374
- service,
375
- showDescription,
376
- showPrice,
377
- showImage,
378
- onSelect
379
- },
380
- service.id
381
- ))
382
- }
383
- );
384
- }
385
-
386
- // src/lib/linkify.tsx
387
- var import_jsx_runtime4 = require("react/jsx-runtime");
388
- var LINK_REGEX = /((https?:\/\/|www\.)[^\s]+|[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}|(?:[a-z0-9-]+\.)+[a-z]{2,}(?:\/[^\s]*)?)/gi;
389
- function buildHref(raw) {
390
- if (raw.includes("@") && !raw.startsWith("http")) {
391
- return `mailto:${raw}`;
392
- }
393
- if (raw.startsWith("http://") || raw.startsWith("https://")) {
394
- return raw;
395
- }
396
- if (raw.startsWith("www.")) {
397
- return `https://${raw}`;
398
- }
399
- return `https://${raw}`;
400
- }
401
- function trimTrailingPunctuation(value) {
402
- let text = value;
403
- let trailing = "";
404
- while (text && /[),.!?:;]+$/.test(text)) {
405
- trailing = text.slice(-1) + trailing;
406
- text = text.slice(0, -1);
407
- }
408
- return { text, trailing };
409
- }
410
- function renderLinkedText(text) {
411
- if (!text) return null;
412
- const matches = Array.from(text.matchAll(LINK_REGEX));
413
- if (matches.length === 0) return text;
414
- const nodes = [];
415
- let lastIndex = 0;
416
- matches.forEach((match, index) => {
417
- const matchText = match[0];
418
- const start = match.index ?? 0;
419
- if (start > lastIndex) {
420
- nodes.push(text.slice(lastIndex, start));
421
- }
422
- const { text: cleanText, trailing } = trimTrailingPunctuation(matchText);
423
- if (cleanText) {
424
- const href = buildHref(cleanText);
425
- nodes.push(
426
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", children: cleanText }, `link-${start}-${index}`)
427
- );
428
- }
429
- if (trailing) {
430
- nodes.push(trailing);
431
- }
432
- lastIndex = start + matchText.length;
433
- });
434
- if (lastIndex < text.length) {
435
- nodes.push(text.slice(lastIndex));
436
- }
437
- return nodes;
438
- }
439
-
440
- // src/components/TeamMember.tsx
441
- var import_jsx_runtime5 = require("react/jsx-runtime");
442
- function TeamMember({
443
- member,
444
- className,
445
- showRole = true,
446
- showEmail = false,
447
- showBio = false,
448
- layout = "card",
449
- imagePosition = "left"
450
- }) {
451
- const initials = member.name?.split(" ").map((part) => part[0]).slice(0, 2).join("").toUpperCase();
452
- const avatar = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "idk-team__avatar", children: member.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: member.photo.url, alt: member.name }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: initials }) });
453
- if (layout === "profile") {
454
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: ["idk-team__profile", className].filter(Boolean).join(" "), children: [
455
- imagePosition === "left" ? avatar : null,
456
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "idk-team__info", children: [
457
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "idk-card__title", children: member.name }),
458
- showRole ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__description", children: member.role || "Staff" }) : null,
459
- showBio && member.bio ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-team__bio", children: renderLinkedText(member.bio) }) : null,
460
- showEmail ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__meta", children: member.email }) : null
461
- ] }),
462
- imagePosition === "right" ? avatar : null
463
- ] });
464
- }
465
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: ["idk-card", className].filter(Boolean).join(" "), children: [
466
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "idk-team__avatar", children: member.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: member.photo.url, alt: member.name }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: initials }) }),
467
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "idk-team__info", children: [
468
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "idk-card__title", children: member.name }),
469
- showRole ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__description", children: member.role || "Staff" }) : null,
470
- showBio && member.bio ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-team__bio", children: renderLinkedText(member.bio) }) : null,
471
- showEmail ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__meta", children: member.email }) : null
472
- ] })
473
- ] });
474
- }
475
-
476
- // src/components/TeamGrid.tsx
477
- var import_jsx_runtime6 = require("react/jsx-runtime");
478
- function TeamGrid({
479
- columns = 3,
480
- className,
481
- showRole,
482
- showEmail,
483
- showBio,
484
- layout = "card",
485
- imagePosition = "left",
486
- query,
487
- filter,
488
- sort,
489
- limit,
490
- emptyMessage = "No team members available.",
491
- loadingMessage = "Loading team..."
492
- }) {
493
- const { data, isLoading, error } = useTeam(query);
494
- if (isLoading) {
495
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "idk-state", children: loadingMessage });
496
- }
497
- let members = data?.team ? [...data.team] : [];
498
- if (filter) {
499
- members = members.filter(filter);
500
- }
501
- if (sort) {
502
- members = members.sort(sort);
503
- }
504
- if (typeof limit === "number") {
505
- members = members.slice(0, Math.max(0, limit));
506
- }
507
- if (error || members.length === 0) {
508
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "idk-state", children: emptyMessage });
509
- }
510
- const gridStyle = { gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))` };
511
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
512
- "div",
513
- {
514
- className: ["idk-team", className].filter(Boolean).join(" "),
515
- style: gridStyle,
516
- children: members.map((member) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
517
- TeamMember,
518
- {
519
- member,
520
- showRole,
521
- showEmail,
522
- showBio,
523
- layout,
524
- imagePosition
525
- },
526
- member.id
527
- ))
528
- }
529
- );
530
- }
531
-
532
- // src/components/ContactForm.tsx
286
+ // src/core/hooks/useBookingWidget.ts
533
287
  var import_react6 = require("react");
534
- var import_react_query7 = require("@tanstack/react-query");
535
- var import_jsx_runtime7 = require("react/jsx-runtime");
536
- var DEFAULT_FIELDS = ["name", "email", "phone", "message"];
537
- function ContactForm({
538
- fields = DEFAULT_FIELDS,
539
- formType = "contact",
540
- submitLabel = "Send Message",
541
- className,
542
- onSuccess,
543
- onError
544
- }) {
545
- const config = usePlatformConfig();
546
- const [formState, setFormState] = (0, import_react6.useState)({
547
- name: "",
548
- email: "",
549
- phone: "",
550
- subject: "",
551
- message: ""
552
- });
553
- const mutation = (0, import_react_query7.useMutation)({
554
- mutationFn: (payload) => apiRequest(config, "/contact", {
555
- method: "POST",
556
- body: JSON.stringify(payload)
557
- }),
558
- onSuccess: () => {
559
- setFormState({ name: "", email: "", phone: "", subject: "", message: "" });
560
- onSuccess?.();
561
- },
562
- onError: (error) => {
563
- const message = error instanceof Error ? error.message : "Failed to submit form";
564
- onError?.(message);
565
- }
566
- });
567
- const handleChange = (field, value) => {
568
- setFormState((prev) => ({ ...prev, [field]: value }));
569
- };
570
- const handleSubmit = (event) => {
571
- event.preventDefault();
572
- mutation.mutate({
573
- name: formState.name,
574
- email: formState.email,
575
- phone: formState.phone || void 0,
576
- subject: formState.subject || void 0,
577
- message: formState.message,
578
- formType
579
- });
580
- };
581
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("form", { className: ["idk-form", className].filter(Boolean).join(" "), onSubmit: handleSubmit, children: [
582
- fields.includes("name") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
583
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Name" }),
584
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
585
- "input",
586
- {
587
- value: formState.name,
588
- onChange: (event) => handleChange("name", event.target.value),
589
- required: true
590
- }
591
- )
592
- ] }),
593
- fields.includes("email") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
594
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Email" }),
595
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
596
- "input",
597
- {
598
- type: "email",
599
- value: formState.email,
600
- onChange: (event) => handleChange("email", event.target.value),
601
- required: true
602
- }
603
- )
604
- ] }),
605
- fields.includes("phone") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
606
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Phone" }),
607
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
608
- "input",
609
- {
610
- value: formState.phone,
611
- onChange: (event) => handleChange("phone", event.target.value)
612
- }
613
- )
614
- ] }),
615
- fields.includes("subject") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
616
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Subject" }),
617
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
618
- "input",
619
- {
620
- value: formState.subject,
621
- onChange: (event) => handleChange("subject", event.target.value)
622
- }
623
- )
624
- ] }),
625
- fields.includes("message") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
626
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Message" }),
627
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
628
- "textarea",
629
- {
630
- value: formState.message,
631
- onChange: (event) => handleChange("message", event.target.value),
632
- rows: 5,
633
- required: true
634
- }
635
- )
636
- ] }),
637
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "submit", className: "idk-button", disabled: mutation.isPending, children: mutation.isPending ? "Sending..." : submitLabel }),
638
- mutation.isError ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-form__error", children: "Something went wrong. Please try again." }) : null,
639
- mutation.isSuccess ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-form__success", children: "Thanks! We'll be in touch soon." }) : null
640
- ] });
641
- }
642
-
643
- // src/components/BookingWidget.tsx
644
- var import_react7 = require("react");
645
- var import_jsx_runtime8 = require("react/jsx-runtime");
646
288
  var parseDateOnly = (value) => {
647
289
  if (!value) return null;
648
290
  const normalized = value.includes("T") ? value.split("T")[0] : value;
@@ -655,92 +297,71 @@ var addDays = (dateStr, days) => {
655
297
  parsed.setDate(parsed.getDate() + days);
656
298
  return parsed.toISOString().slice(0, 10);
657
299
  };
658
- function BookingWidget({
659
- className,
660
- showStaffSelection = true,
661
- onSuccess,
662
- services,
663
- servicesQuery,
664
- teamQuery,
665
- serviceFilter,
666
- teamFilter,
667
- title = "Book an Appointment",
668
- subtitle,
669
- autoAdvanceOnSelect = false,
670
- scrollToStep = true,
671
- scrollOffset = 96,
672
- enableServiceSearch = true,
673
- serviceSearchPlaceholder = "Search services",
674
- enableCategoryFilter = false,
675
- categoryLabel = "All categories",
676
- serviceListMaxHeight,
677
- serviceLayout = "list",
678
- serviceColumns = 2,
679
- servicePageSize = 10,
680
- showServicePagination,
681
- staffLayout = "grid",
682
- staffColumns = 2,
683
- showAnyStaffOption = true,
684
- staffPageSize = 8,
685
- showStaffPagination,
686
- timeLayout = "split",
687
- availabilityDays: availabilityDaysProp = 7,
688
- enableDatePaging = true,
689
- datePickerMode = "list",
690
- calendarVariant = "default",
691
- maxAdvanceMonths = 3,
692
- disabledWeekdays = [],
693
- showFullyBookedLabel = true
694
- }) {
695
- const resolvedServicesQuery = (0, import_react7.useMemo)(
696
- () => services ? { ...servicesQuery, enabled: false } : servicesQuery,
697
- [services, servicesQuery]
300
+ function useBookingWidget(options = {}) {
301
+ const {
302
+ services: servicesProp,
303
+ servicesQuery,
304
+ teamQuery,
305
+ serviceFilter,
306
+ teamFilter,
307
+ showStaffSelection = true,
308
+ autoAdvanceOnSelect = false,
309
+ scrollToStep: scrollToStepEnabled = true,
310
+ scrollOffset = 96,
311
+ widgetRef,
312
+ onSuccess,
313
+ availabilityDays: availabilityDaysProp = 7,
314
+ enableDatePaging = true,
315
+ datePickerMode = "list",
316
+ calendarVariant = "default",
317
+ maxAdvanceMonths = 3,
318
+ disabledWeekdays = [],
319
+ showFullyBookedLabel = true,
320
+ enableServiceSearch = true,
321
+ enableCategoryFilter = false,
322
+ servicePageSize = 10,
323
+ staffPageSize = 8,
324
+ showAnyStaffOption = true
325
+ } = options;
326
+ const resolvedServicesQuery = (0, import_react6.useMemo)(
327
+ () => servicesProp ? { ...servicesQuery, enabled: false } : servicesQuery,
328
+ [servicesProp, servicesQuery]
698
329
  );
699
330
  const { data: servicesData, isLoading: servicesLoading } = useServices(resolvedServicesQuery);
700
331
  const { data: teamData } = useTeam(teamQuery);
701
332
  const createBooking = useCreateBooking();
702
- const widgetRef = (0, import_react7.useRef)(null);
703
- const [step, setStep] = (0, import_react7.useState)("service");
704
- const [selectedService, setSelectedService] = (0, import_react7.useState)(null);
705
- const [selectedStaff, setSelectedStaff] = (0, import_react7.useState)(null);
706
- const [selectedDate, setSelectedDate] = (0, import_react7.useState)(null);
707
- const [selectedTime, setSelectedTime] = (0, import_react7.useState)(null);
708
- const [selectedEndTime, setSelectedEndTime] = (0, import_react7.useState)(null);
709
- const [serviceSearch, setServiceSearch] = (0, import_react7.useState)("");
710
- const [categoryFilter, setCategoryFilter] = (0, import_react7.useState)("all");
711
- const [servicePage, setServicePage] = (0, import_react7.useState)(1);
712
- const [staffPage, setStaffPage] = (0, import_react7.useState)(1);
713
- const [availabilityDays, setAvailabilityDays] = (0, import_react7.useState)(
714
- Math.max(1, availabilityDaysProp)
715
- );
716
- const [availabilityStartDate, setAvailabilityStartDate] = (0, import_react7.useState)(
333
+ const [step, setStep] = (0, import_react6.useState)("service");
334
+ const [selectedService, setSelectedService] = (0, import_react6.useState)(null);
335
+ const [selectedStaff, setSelectedStaff] = (0, import_react6.useState)(null);
336
+ const [selectedDate, setSelectedDate] = (0, import_react6.useState)(null);
337
+ const [selectedTime, setSelectedTime] = (0, import_react6.useState)(null);
338
+ const [selectedEndTime, setSelectedEndTime] = (0, import_react6.useState)(null);
339
+ const [serviceSearch, setServiceSearch] = (0, import_react6.useState)("");
340
+ const [categoryFilter, setCategoryFilter] = (0, import_react6.useState)("all");
341
+ const [servicePage, setServicePage] = (0, import_react6.useState)(1);
342
+ const [staffPage, setStaffPage] = (0, import_react6.useState)(1);
343
+ const [availabilityDays, setAvailabilityDays] = (0, import_react6.useState)(Math.max(1, availabilityDaysProp));
344
+ const [availabilityStartDate, setAvailabilityStartDate] = (0, import_react6.useState)(
717
345
  () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
718
346
  );
719
- const [calendarMonth, setCalendarMonth] = (0, import_react7.useState)(() => {
347
+ const [calendarMonth, setCalendarMonth] = (0, import_react6.useState)(() => {
720
348
  const now = /* @__PURE__ */ new Date();
721
349
  return new Date(now.getFullYear(), now.getMonth(), 1);
722
350
  });
723
- const [bookingError, setBookingError] = (0, import_react7.useState)(null);
724
- const [details, setDetails] = (0, import_react7.useState)({
725
- name: "",
726
- email: "",
727
- phone: "",
728
- notes: ""
729
- });
730
- const servicesList = (0, import_react7.useMemo)(() => {
731
- const list = services ? [...services] : servicesData?.services ? [...servicesData.services] : [];
351
+ const [bookingError, setBookingError] = (0, import_react6.useState)(null);
352
+ const [details, setDetails] = (0, import_react6.useState)({ name: "", email: "", phone: "", notes: "" });
353
+ const servicesList = (0, import_react6.useMemo)(() => {
354
+ const list = servicesProp ? [...servicesProp] : servicesData?.services ? [...servicesData.services] : [];
732
355
  return serviceFilter ? list.filter(serviceFilter) : list;
733
- }, [services, servicesData, serviceFilter]);
734
- const categories = (0, import_react7.useMemo)(() => {
356
+ }, [servicesProp, servicesData, serviceFilter]);
357
+ const categories = (0, import_react6.useMemo)(() => {
735
358
  const set = /* @__PURE__ */ new Set();
736
- servicesList.forEach((service) => {
737
- if (service.category) {
738
- set.add(service.category);
739
- }
359
+ servicesList.forEach((s) => {
360
+ if (s.category) set.add(s.category);
740
361
  });
741
362
  return Array.from(set).sort();
742
363
  }, [servicesList]);
743
- const filteredServices = (0, import_react7.useMemo)(() => {
364
+ const filteredServices = (0, import_react6.useMemo)(() => {
744
365
  const search = serviceSearch.trim().toLowerCase();
745
366
  return servicesList.filter((service) => {
746
367
  const matchesSearch = !search || service.name.toLowerCase().includes(search) || (service.description || "").toLowerCase().includes(search) || (service.category || "").toLowerCase().includes(search);
@@ -748,19 +369,18 @@ function BookingWidget({
748
369
  return matchesSearch && matchesCategory;
749
370
  });
750
371
  }, [servicesList, serviceSearch, categoryFilter]);
751
- (0, import_react7.useEffect)(() => {
372
+ (0, import_react6.useEffect)(() => {
752
373
  setServicePage(1);
753
374
  }, [serviceSearch, categoryFilter]);
754
- (0, import_react7.useEffect)(() => {
375
+ (0, import_react6.useEffect)(() => {
755
376
  setStaffPage(1);
756
377
  }, [selectedService?.id, teamData, teamFilter]);
757
- const pagedServices = (0, import_react7.useMemo)(() => {
378
+ const pagedServices = (0, import_react6.useMemo)(() => {
758
379
  const pageSize = Math.max(1, servicePageSize);
759
380
  return filteredServices.slice(0, pageSize * servicePage);
760
381
  }, [filteredServices, servicePage, servicePageSize]);
761
382
  const hasMoreServices = filteredServices.length > pagedServices.length;
762
- const shouldPaginate = showServicePagination ?? hasMoreServices;
763
- const staffOptions = (0, import_react7.useMemo)(() => {
383
+ const staffOptions = (0, import_react6.useMemo)(() => {
764
384
  if (!selectedService) return [];
765
385
  if (selectedService.assignedStaff && selectedService.assignedStaff.length > 0) {
766
386
  const assigned = selectedService.assignedStaff;
@@ -769,66 +389,12 @@ function BookingWidget({
769
389
  const teamList = teamData?.team || [];
770
390
  return teamFilter ? teamList.filter(teamFilter) : teamList;
771
391
  }, [selectedService, teamData, teamFilter]);
772
- const pagedStaff = (0, import_react7.useMemo)(() => {
392
+ const pagedStaff = (0, import_react6.useMemo)(() => {
773
393
  const pageSize = Math.max(1, staffPageSize);
774
394
  return staffOptions.slice(0, pageSize * staffPage);
775
395
  }, [staffOptions, staffPage, staffPageSize]);
776
396
  const hasMoreStaff = staffOptions.length > pagedStaff.length;
777
- const shouldPaginateStaff = showStaffPagination ?? hasMoreStaff;
778
- const availabilityEndDate = (0, import_react7.useMemo)(
779
- () => addDays(availabilityStartDate, Math.max(1, availabilityDays) - 1),
780
- [availabilityStartDate, availabilityDays]
781
- );
782
- const availability = useAvailability({
783
- serviceId: selectedService?.id || "",
784
- staffId: selectedStaff?.id,
785
- startDate: availabilityStartDate,
786
- endDate: availabilityEndDate,
787
- enabled: Boolean(selectedService)
788
- });
789
- const timeZone = availability.data?.timeZone || "America/Chicago";
790
- const safeTimeZone = (0, import_react7.useMemo)(() => {
791
- if (!timeZone) return void 0;
792
- try {
793
- new Intl.DateTimeFormat("en-US", { timeZone }).format(/* @__PURE__ */ new Date());
794
- return timeZone;
795
- } catch {
796
- return void 0;
797
- }
798
- }, [timeZone]);
799
- const timeZoneLabel = (0, import_react7.useMemo)(() => {
800
- if (timeZone) return timeZone;
801
- try {
802
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
803
- } catch {
804
- return "Local time";
805
- }
806
- }, [timeZone]);
807
- const dateLabelFormatter = (0, import_react7.useMemo)(
808
- () => new Intl.DateTimeFormat("en-US", {
809
- weekday: "short",
810
- month: "short",
811
- day: "numeric"
812
- }),
813
- []
814
- );
815
- const dateHeadingFormatter = (0, import_react7.useMemo)(
816
- () => new Intl.DateTimeFormat("en-US", {
817
- weekday: "long",
818
- month: "long",
819
- day: "numeric"
820
- }),
821
- []
822
- );
823
- const timeFormatter = (0, import_react7.useMemo)(
824
- () => new Intl.DateTimeFormat("en-US", {
825
- timeZone: safeTimeZone,
826
- hour: "numeric",
827
- minute: "2-digit"
828
- }),
829
- [safeTimeZone]
830
- );
831
- const requiresStaff = showStaffSelection || selectedService?.requiresStaffSelection || selectedService?.schedulingType === "customer-choice";
397
+ const requiresStaff = showStaffSelection || Boolean(selectedService?.requiresStaffSelection) || selectedService?.schedulingType === "customer-choice";
832
398
  const canSkipStaff = !selectedService?.requiresStaffSelection;
833
399
  const totalSteps = requiresStaff ? 4 : 3;
834
400
  const stepNumber = (() => {
@@ -837,81 +403,122 @@ function BookingWidget({
837
403
  if (step === "time") return requiresStaff ? 3 : 2;
838
404
  if (step === "details") return requiresStaff ? 4 : 3;
839
405
  return totalSteps;
840
- })();
841
- const scrollToTop = (0, import_react7.useCallback)(() => {
842
- if (!scrollToStep || !widgetRef.current) return;
843
- const top = widgetRef.current.getBoundingClientRect().top + window.scrollY;
844
- window.scrollTo({
845
- top: Math.max(0, top - scrollOffset),
846
- behavior: "smooth"
847
- });
848
- }, [scrollToStep, scrollOffset]);
849
- const resetSelections = () => {
850
- setSelectedStaff(null);
851
- setSelectedDate(null);
852
- setSelectedTime(null);
853
- setSelectedEndTime(null);
854
- setBookingError(null);
855
- };
856
- const parseDateTime = (value, fallbackDate) => {
857
- if (!value) return null;
858
- const direct = new Date(value);
859
- if (!Number.isNaN(direct.getTime())) return direct;
860
- if (fallbackDate) {
861
- const normalized = fallbackDate.includes("T") ? fallbackDate.split("T")[0] : fallbackDate;
862
- const combined = /* @__PURE__ */ new Date(`${normalized}T${value}`);
863
- if (!Number.isNaN(combined.getTime())) return combined;
864
- }
865
- return null;
866
- };
867
- const formatDateLabel = (date) => {
868
- const parsed = parseDateOnly(date);
869
- if (!parsed) return date;
870
- try {
871
- return dateLabelFormatter.format(parsed);
872
- } catch {
873
- return date;
874
- }
875
- };
876
- const formatDateHeading = (date) => {
877
- const parsed = parseDateOnly(date);
878
- if (!parsed) return date;
406
+ })();
407
+ const availabilityEndDate = (0, import_react6.useMemo)(
408
+ () => addDays(availabilityStartDate, Math.max(1, availabilityDays) - 1),
409
+ [availabilityStartDate, availabilityDays]
410
+ );
411
+ const availability = useAvailability({
412
+ serviceId: selectedService?.id || "",
413
+ staffId: selectedStaff?.id,
414
+ startDate: availabilityStartDate,
415
+ endDate: availabilityEndDate,
416
+ enabled: Boolean(selectedService)
417
+ });
418
+ const timeZone = availability.data?.timeZone || "America/Chicago";
419
+ const safeTimeZone = (0, import_react6.useMemo)(() => {
420
+ if (!timeZone) return void 0;
879
421
  try {
880
- return dateHeadingFormatter.format(parsed);
422
+ new Intl.DateTimeFormat("en-US", { timeZone }).format(/* @__PURE__ */ new Date());
423
+ return timeZone;
881
424
  } catch {
882
- return date;
425
+ return void 0;
883
426
  }
884
- };
885
- const formatTimeLabel = (iso) => {
886
- const parsed = parseDateTime(iso, selectedDate);
887
- if (!parsed) return iso;
427
+ }, [timeZone]);
428
+ const timeZoneLabel = (0, import_react6.useMemo)(() => {
429
+ if (timeZone) return timeZone;
888
430
  try {
889
- return timeFormatter.format(parsed);
431
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
890
432
  } catch {
891
- return parsed.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
433
+ return "Local time";
892
434
  }
893
- };
894
- const formatDuration = (minutes) => {
435
+ }, [timeZone]);
436
+ const availabilityByDate = (0, import_react6.useMemo)(() => {
437
+ const map = /* @__PURE__ */ new Map();
438
+ (availability.data?.dates || []).forEach((entry) => {
439
+ map.set(entry.date, entry);
440
+ });
441
+ return map;
442
+ }, [availability.data]);
443
+ const dateLabelFormatter = (0, import_react6.useMemo)(
444
+ () => new Intl.DateTimeFormat("en-US", { weekday: "short", month: "short", day: "numeric" }),
445
+ []
446
+ );
447
+ const dateHeadingFormatter = (0, import_react6.useMemo)(
448
+ () => new Intl.DateTimeFormat("en-US", { weekday: "long", month: "long", day: "numeric" }),
449
+ []
450
+ );
451
+ const timeFormatter = (0, import_react6.useMemo)(
452
+ () => new Intl.DateTimeFormat("en-US", { timeZone: safeTimeZone, hour: "numeric", minute: "2-digit" }),
453
+ [safeTimeZone]
454
+ );
455
+ const parseDateTime = (0, import_react6.useCallback)(
456
+ (value, fallbackDate) => {
457
+ if (!value) return null;
458
+ const direct = new Date(value);
459
+ if (!Number.isNaN(direct.getTime())) return direct;
460
+ if (fallbackDate) {
461
+ const normalized = fallbackDate.includes("T") ? fallbackDate.split("T")[0] : fallbackDate;
462
+ const combined = /* @__PURE__ */ new Date(`${normalized}T${value}`);
463
+ if (!Number.isNaN(combined.getTime())) return combined;
464
+ }
465
+ return null;
466
+ },
467
+ []
468
+ );
469
+ const formatDateLabel = (0, import_react6.useCallback)(
470
+ (date) => {
471
+ const parsed = parseDateOnly(date);
472
+ if (!parsed) return date;
473
+ try {
474
+ return dateLabelFormatter.format(parsed);
475
+ } catch {
476
+ return date;
477
+ }
478
+ },
479
+ [dateLabelFormatter]
480
+ );
481
+ const formatDateHeading = (0, import_react6.useCallback)(
482
+ (date) => {
483
+ const parsed = parseDateOnly(date);
484
+ if (!parsed) return date;
485
+ try {
486
+ return dateHeadingFormatter.format(parsed);
487
+ } catch {
488
+ return date;
489
+ }
490
+ },
491
+ [dateHeadingFormatter]
492
+ );
493
+ const formatTimeLabel = (0, import_react6.useCallback)(
494
+ (iso) => {
495
+ const parsed = parseDateTime(iso, selectedDate);
496
+ if (!parsed) return iso;
497
+ try {
498
+ return timeFormatter.format(parsed);
499
+ } catch {
500
+ return parsed.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
501
+ }
502
+ },
503
+ [parseDateTime, selectedDate, timeFormatter]
504
+ );
505
+ const formatDuration = (0, import_react6.useCallback)((minutes) => {
895
506
  if (!Number.isFinite(minutes)) return "";
896
507
  if (minutes < 60) return `${minutes} min`;
897
508
  const hours = Math.floor(minutes / 60);
898
509
  const remaining = minutes % 60;
899
510
  return remaining === 0 ? `${hours} hr${hours > 1 ? "s" : ""}` : `${hours} hr ${remaining} min`;
900
- };
901
- const todayString = (0, import_react7.useMemo)(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), []);
902
- const todayDate = (0, import_react7.useMemo)(() => {
511
+ }, []);
512
+ const todayString = (0, import_react6.useMemo)(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), []);
513
+ const todayDate = (0, import_react6.useMemo)(() => {
903
514
  const now = /* @__PURE__ */ new Date();
904
515
  now.setHours(0, 0, 0, 0);
905
516
  return now;
906
517
  }, []);
907
- const disabledWeekdaysSet = (0, import_react7.useMemo)(() => new Set(disabledWeekdays), [disabledWeekdays]);
908
- const getMonthStart = (0, import_react7.useCallback)((date) => {
909
- return new Date(date.getFullYear(), date.getMonth(), 1);
910
- }, []);
911
- const getMonthEnd = (0, import_react7.useCallback)((date) => {
912
- return new Date(date.getFullYear(), date.getMonth() + 1, 0);
913
- }, []);
914
- const getMonthDays = (0, import_react7.useCallback)(
518
+ const disabledWeekdaysSet = (0, import_react6.useMemo)(() => new Set(disabledWeekdays), [disabledWeekdays]);
519
+ const getMonthStart = (0, import_react6.useCallback)((date) => new Date(date.getFullYear(), date.getMonth(), 1), []);
520
+ const getMonthEnd = (0, import_react6.useCallback)((date) => new Date(date.getFullYear(), date.getMonth() + 1, 0), []);
521
+ const getMonthDays = (0, import_react6.useCallback)(
915
522
  (date) => {
916
523
  const start = getMonthStart(date);
917
524
  const end = getMonthEnd(date);
@@ -936,20 +543,20 @@ function BookingWidget({
936
543
  },
937
544
  [getMonthEnd, getMonthStart]
938
545
  );
939
- const maxAdvanceDate = (0, import_react7.useMemo)(() => {
546
+ const maxAdvanceDate = (0, import_react6.useMemo)(() => {
940
547
  if (!maxAdvanceMonths || maxAdvanceMonths <= 0) return null;
941
548
  const maxMonth = new Date(todayDate.getFullYear(), todayDate.getMonth() + maxAdvanceMonths - 1, 1);
942
549
  return getMonthEnd(maxMonth);
943
550
  }, [getMonthEnd, maxAdvanceMonths, todayDate]);
944
- const maxAdvanceMonthStart = (0, import_react7.useMemo)(() => {
551
+ const maxAdvanceMonthStart = (0, import_react6.useMemo)(() => {
945
552
  if (!maxAdvanceMonths || maxAdvanceMonths <= 0) return null;
946
553
  return new Date(todayDate.getFullYear(), todayDate.getMonth() + maxAdvanceMonths - 1, 1);
947
554
  }, [maxAdvanceMonths, todayDate]);
948
- const monthOptionCount = (0, import_react7.useMemo)(() => {
555
+ const monthOptionCount = (0, import_react6.useMemo)(() => {
949
556
  if (!maxAdvanceMonths || maxAdvanceMonths <= 0) return 12;
950
557
  return maxAdvanceMonths;
951
558
  }, [maxAdvanceMonths]);
952
- const monthOptions = (0, import_react7.useMemo)(() => {
559
+ const monthOptions = (0, import_react6.useMemo)(() => {
953
560
  return Array.from({ length: monthOptionCount }, (_, index) => {
954
561
  const date = new Date(todayDate.getFullYear(), todayDate.getMonth() + index, 1);
955
562
  return {
@@ -959,700 +566,1153 @@ function BookingWidget({
959
566
  };
960
567
  });
961
568
  }, [monthOptionCount, todayDate]);
962
- const calendarDays = (0, import_react7.useMemo)(() => getMonthDays(calendarMonth), [getMonthDays, calendarMonth]);
963
- const availabilityByDate = (0, import_react7.useMemo)(() => {
964
- const map = /* @__PURE__ */ new Map();
965
- (availability.data?.dates || []).forEach((entry) => {
966
- map.set(entry.date, entry);
967
- });
968
- return map;
969
- }, [availability.data]);
970
- const hasBlockedInCalendarView = (0, import_react7.useMemo)(() => {
569
+ const calendarDays = (0, import_react6.useMemo)(() => getMonthDays(calendarMonth), [getMonthDays, calendarMonth]);
570
+ const hasBlockedInCalendarView = (0, import_react6.useMemo)(() => {
971
571
  return calendarDays.some((day) => {
972
572
  const entry = availabilityByDate.get(day.date);
973
573
  if (!entry) return false;
974
574
  return !entry.slots.some((slot) => slot.available);
975
575
  });
976
576
  }, [calendarDays, availabilityByDate]);
977
- const handlePrevDates = () => {
577
+ const scrollToTop = (0, import_react6.useCallback)(() => {
578
+ if (!scrollToStepEnabled || !widgetRef?.current) return;
579
+ const top = widgetRef.current.getBoundingClientRect().top + window.scrollY;
580
+ window.scrollTo({ top: Math.max(0, top - scrollOffset), behavior: "smooth" });
581
+ }, [scrollToStepEnabled, scrollOffset, widgetRef]);
582
+ const resetSelections = (0, import_react6.useCallback)(() => {
583
+ setSelectedStaff(null);
584
+ setSelectedDate(null);
585
+ setSelectedTime(null);
586
+ setSelectedEndTime(null);
587
+ setBookingError(null);
588
+ }, []);
589
+ (0, import_react6.useEffect)(() => {
590
+ if (step !== "time") return;
591
+ if (!availability.data?.dates?.length) return;
592
+ const matches = selectedDate ? availability.data.dates.some((entry) => entry.date === selectedDate) : false;
593
+ if (matches) return;
594
+ setSelectedDate(availability.data.dates[0].date);
595
+ }, [step, availability.data, selectedDate]);
596
+ (0, import_react6.useEffect)(() => {
597
+ if (!selectedService) return;
598
+ if (datePickerMode === "calendar") {
599
+ const now = /* @__PURE__ */ new Date();
600
+ setCalendarMonth(new Date(now.getFullYear(), now.getMonth(), 1));
601
+ return;
602
+ }
603
+ setAvailabilityStartDate((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
604
+ setAvailabilityDays(Math.max(1, availabilityDaysProp));
605
+ }, [selectedService?.id, selectedStaff?.id, availabilityDaysProp, datePickerMode]);
606
+ (0, import_react6.useEffect)(() => {
607
+ if (datePickerMode !== "calendar") return;
608
+ const start = getMonthStart(calendarMonth);
609
+ const end = getMonthEnd(calendarMonth);
610
+ setAvailabilityStartDate(start.toISOString().slice(0, 10));
611
+ setAvailabilityDays(end.getDate());
612
+ }, [calendarMonth, datePickerMode, getMonthEnd, getMonthStart]);
613
+ const selectService = (0, import_react6.useCallback)(
614
+ (service) => {
615
+ setSelectedService(service);
616
+ resetSelections();
617
+ if (autoAdvanceOnSelect) {
618
+ if (requiresStaff) setStep("staff");
619
+ else setStep("time");
620
+ scrollToTop();
621
+ }
622
+ },
623
+ [autoAdvanceOnSelect, requiresStaff, resetSelections, scrollToTop]
624
+ );
625
+ const selectStaff = (0, import_react6.useCallback)(
626
+ (staff) => {
627
+ setSelectedStaff(staff);
628
+ setSelectedDate(null);
629
+ setSelectedTime(null);
630
+ setSelectedEndTime(null);
631
+ setBookingError(null);
632
+ if (autoAdvanceOnSelect) {
633
+ setStep("time");
634
+ scrollToTop();
635
+ }
636
+ },
637
+ [autoAdvanceOnSelect, scrollToTop]
638
+ );
639
+ const selectDate = (0, import_react6.useCallback)((date) => {
640
+ setSelectedDate(date);
641
+ setSelectedTime(null);
642
+ setSelectedEndTime(null);
643
+ setBookingError(null);
644
+ }, []);
645
+ const selectTime = (0, import_react6.useCallback)(
646
+ (time, endTime) => {
647
+ setSelectedTime(time);
648
+ setSelectedEndTime(endTime);
649
+ setBookingError(null);
650
+ if (autoAdvanceOnSelect) {
651
+ setStep("details");
652
+ scrollToTop();
653
+ }
654
+ },
655
+ [autoAdvanceOnSelect, scrollToTop]
656
+ );
657
+ const continueFromService = (0, import_react6.useCallback)(() => {
658
+ if (!selectedService) return;
659
+ if (requiresStaff) setStep("staff");
660
+ else setStep("time");
661
+ scrollToTop();
662
+ }, [selectedService, requiresStaff, scrollToTop]);
663
+ const continueFromStaff = (0, import_react6.useCallback)(() => {
664
+ if (!canSkipStaff && !selectedStaff) return;
665
+ setStep("time");
666
+ scrollToTop();
667
+ }, [canSkipStaff, selectedStaff, scrollToTop]);
668
+ const continueFromTime = (0, import_react6.useCallback)(() => {
669
+ if (!selectedTime || !selectedEndTime) return;
670
+ setStep("details");
671
+ scrollToTop();
672
+ }, [selectedTime, selectedEndTime, scrollToTop]);
673
+ const goBack = (0, import_react6.useCallback)(() => {
674
+ if (step === "service") return;
675
+ if (step === "staff") setStep("service");
676
+ else if (step === "time") setStep(requiresStaff ? "staff" : "service");
677
+ else if (step === "details") setStep("time");
678
+ else if (step === "done") setStep("details");
679
+ setBookingError(null);
680
+ scrollToTop();
681
+ }, [step, requiresStaff, scrollToTop]);
682
+ const goToServiceStep = (0, import_react6.useCallback)(() => {
683
+ setStep("service");
684
+ scrollToTop();
685
+ }, [scrollToTop]);
686
+ const reset = (0, import_react6.useCallback)(() => {
687
+ setStep("service");
688
+ setSelectedService(null);
689
+ resetSelections();
690
+ setDetails({ name: "", email: "", phone: "", notes: "" });
691
+ scrollToTop();
692
+ }, [resetSelections, scrollToTop]);
693
+ const submitBooking = (0, import_react6.useCallback)(
694
+ async (event) => {
695
+ event?.preventDefault();
696
+ if (!selectedService || !selectedDate || !selectedTime || !selectedEndTime) return;
697
+ const start = parseDateTime(selectedTime, selectedDate);
698
+ const end = parseDateTime(selectedEndTime, selectedDate);
699
+ if (!start || !end) {
700
+ setBookingError("Please select a valid time slot.");
701
+ return;
702
+ }
703
+ try {
704
+ setBookingError(null);
705
+ const result = await createBooking.mutateAsync({
706
+ serviceId: selectedService.id,
707
+ staffId: selectedStaff?.id,
708
+ startTime: start.toISOString(),
709
+ endTime: end.toISOString(),
710
+ customerName: details.name,
711
+ customerEmail: details.email,
712
+ customerPhone: details.phone || void 0,
713
+ customerNotes: details.notes || void 0
714
+ });
715
+ setStep("done");
716
+ onSuccess?.(result?.booking);
717
+ scrollToTop();
718
+ } catch (error) {
719
+ setBookingError(
720
+ error instanceof Error ? error.message : "Unable to complete booking. Please try again."
721
+ );
722
+ }
723
+ },
724
+ [selectedService, selectedDate, selectedTime, selectedEndTime, selectedStaff, details, createBooking, onSuccess, scrollToTop, parseDateTime]
725
+ );
726
+ const handlePrevDates = (0, import_react6.useCallback)(() => {
978
727
  if (availabilityStartDate <= todayString) return;
979
728
  setAvailabilityStartDate((prev) => addDays(prev, -availabilityDays));
980
729
  setSelectedDate(null);
981
730
  setSelectedTime(null);
982
731
  setSelectedEndTime(null);
983
732
  setBookingError(null);
984
- };
985
- const handleNextDates = () => {
733
+ }, [availabilityStartDate, availabilityDays, todayString]);
734
+ const handleNextDates = (0, import_react6.useCallback)(() => {
986
735
  if (maxAdvanceDate) {
987
736
  const nextStart = parseDateOnly(addDays(availabilityStartDate, availabilityDays));
988
- if (nextStart && nextStart > maxAdvanceDate) {
989
- return;
990
- }
737
+ if (nextStart && nextStart > maxAdvanceDate) return;
738
+ }
739
+ setAvailabilityStartDate((prev) => addDays(prev, availabilityDays));
740
+ setSelectedDate(null);
741
+ setSelectedTime(null);
742
+ setSelectedEndTime(null);
743
+ setBookingError(null);
744
+ }, [availabilityStartDate, availabilityDays, maxAdvanceDate]);
745
+ const handlePrevMonth = (0, import_react6.useCallback)(() => {
746
+ setCalendarMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() - 1, 1));
747
+ setSelectedDate(null);
748
+ setSelectedTime(null);
749
+ setSelectedEndTime(null);
750
+ setBookingError(null);
751
+ }, []);
752
+ const handleNextMonth = (0, import_react6.useCallback)(() => {
753
+ if (maxAdvanceMonthStart) {
754
+ const nextMonth = new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() + 1, 1);
755
+ if (nextMonth > maxAdvanceMonthStart) return;
756
+ }
757
+ setCalendarMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() + 1, 1));
758
+ setSelectedDate(null);
759
+ setSelectedTime(null);
760
+ setSelectedEndTime(null);
761
+ setBookingError(null);
762
+ }, [calendarMonth, maxAdvanceMonthStart]);
763
+ const handleMonthSelect = (0, import_react6.useCallback)((value) => {
764
+ const [year, month] = value.split("-").map(Number);
765
+ if (!year || !month) return;
766
+ setCalendarMonth(new Date(year, month - 1, 1));
767
+ setSelectedDate(null);
768
+ setSelectedTime(null);
769
+ setSelectedEndTime(null);
770
+ setBookingError(null);
771
+ }, []);
772
+ const loadMoreServices = (0, import_react6.useCallback)(() => setServicePage((p) => p + 1), []);
773
+ const loadMoreStaff = (0, import_react6.useCallback)(() => setStaffPage((p) => hasMoreStaff ? p + 1 : 1), [hasMoreStaff]);
774
+ return {
775
+ step,
776
+ stepNumber,
777
+ totalSteps,
778
+ selectedService,
779
+ selectedStaff,
780
+ selectedDate,
781
+ selectedTime,
782
+ selectedEndTime,
783
+ details,
784
+ servicesList,
785
+ filteredServices,
786
+ pagedServices,
787
+ hasMoreServices,
788
+ categories,
789
+ staffOptions,
790
+ pagedStaff,
791
+ hasMoreStaff,
792
+ availabilityDates: availability.data?.dates || [],
793
+ availabilityByDate,
794
+ timeZone,
795
+ timeZoneLabel,
796
+ calendarMonth,
797
+ calendarDays,
798
+ hasBlockedInCalendarView,
799
+ maxAdvanceDate,
800
+ maxAdvanceMonthStart,
801
+ monthOptions,
802
+ isServicesLoading: servicesProp ? false : servicesLoading,
803
+ isAvailabilityLoading: availability.isLoading,
804
+ isSubmitting: createBooking.isPending,
805
+ bookingError,
806
+ requiresStaff,
807
+ canSkipStaff,
808
+ serviceSearch,
809
+ categoryFilter,
810
+ servicePage,
811
+ staffPage,
812
+ availabilityStartDate,
813
+ formatDateLabel,
814
+ formatDateHeading,
815
+ formatTimeLabel,
816
+ formatDuration,
817
+ setServiceSearch,
818
+ setCategoryFilter,
819
+ loadMoreServices,
820
+ loadMoreStaff,
821
+ selectService,
822
+ selectStaff,
823
+ selectDate,
824
+ selectTime,
825
+ setDetails,
826
+ goToServiceStep,
827
+ continueFromService,
828
+ continueFromStaff,
829
+ continueFromTime,
830
+ submitBooking,
831
+ goBack,
832
+ reset,
833
+ scrollToTop,
834
+ handlePrevDates,
835
+ handleNextDates,
836
+ handlePrevMonth,
837
+ handleNextMonth,
838
+ handleMonthSelect
839
+ };
840
+ }
841
+
842
+ // src/components/ServiceCard.tsx
843
+ var import_jsx_runtime2 = require("react/jsx-runtime");
844
+ function ServiceCard({
845
+ service,
846
+ className,
847
+ showDescription = true,
848
+ showPrice = true,
849
+ showImage = false,
850
+ onSelect
851
+ }) {
852
+ const handleClick = () => {
853
+ if (onSelect) onSelect(service);
854
+ };
855
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
856
+ "div",
857
+ {
858
+ className: ["idk-card", onSelect ? "idk-card--clickable" : "", className].filter(Boolean).join(" "),
859
+ onClick: onSelect ? handleClick : void 0,
860
+ role: onSelect ? "button" : void 0,
861
+ tabIndex: onSelect ? 0 : void 0,
862
+ children: [
863
+ 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,
864
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "idk-card__header", children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "idk-card__title", children: service.name }),
866
+ showPrice && typeof service.price === "number" && service.price > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "idk-card__price", children: [
867
+ "$",
868
+ (service.price / 100).toFixed(2)
869
+ ] }) : null
870
+ ] }),
871
+ showDescription && service.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "idk-card__description", children: service.description }) : null,
872
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "idk-card__meta", children: [
873
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
874
+ service.duration,
875
+ " min"
876
+ ] }),
877
+ service.category ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: service.category }) : null
878
+ ] })
879
+ ]
880
+ }
881
+ );
882
+ }
883
+
884
+ // src/components/ServicesList.tsx
885
+ var import_jsx_runtime3 = require("react/jsx-runtime");
886
+ function ServicesList({
887
+ layout = "grid",
888
+ columns = 3,
889
+ className,
890
+ showDescription,
891
+ showPrice,
892
+ showImage,
893
+ query,
894
+ filter,
895
+ sort,
896
+ limit,
897
+ emptyMessage = "No services available.",
898
+ loadingMessage = "Loading services...",
899
+ onSelect
900
+ }) {
901
+ const { data, isLoading, error } = useServices(query);
902
+ if (isLoading) {
903
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "idk-state", children: loadingMessage });
904
+ }
905
+ let services = data?.services ? [...data.services] : [];
906
+ if (filter) {
907
+ services = services.filter(filter);
908
+ }
909
+ if (sort) {
910
+ services = services.sort(sort);
911
+ }
912
+ if (typeof limit === "number") {
913
+ services = services.slice(0, Math.max(0, limit));
914
+ }
915
+ if (error || services.length === 0) {
916
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "idk-state", children: emptyMessage });
917
+ }
918
+ const gridStyle = layout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))` } : void 0;
919
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
920
+ "div",
921
+ {
922
+ className: [
923
+ "idk-services",
924
+ layout === "grid" ? "idk-services--grid" : "idk-services--list",
925
+ className
926
+ ].filter(Boolean).join(" "),
927
+ style: gridStyle,
928
+ children: services.map((service) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
929
+ ServiceCard,
930
+ {
931
+ service,
932
+ showDescription,
933
+ showPrice,
934
+ showImage,
935
+ onSelect
936
+ },
937
+ service.id
938
+ ))
939
+ }
940
+ );
941
+ }
942
+
943
+ // src/lib/linkify.tsx
944
+ var import_jsx_runtime4 = require("react/jsx-runtime");
945
+ var LINK_REGEX = /((https?:\/\/|www\.)[^\s]+|[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}|(?:[a-z0-9-]+\.)+[a-z]{2,}(?:\/[^\s]*)?)/gi;
946
+ function buildHref(raw) {
947
+ if (raw.includes("@") && !raw.startsWith("http")) {
948
+ return `mailto:${raw}`;
949
+ }
950
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
951
+ return raw;
952
+ }
953
+ if (raw.startsWith("www.")) {
954
+ return `https://${raw}`;
955
+ }
956
+ return `https://${raw}`;
957
+ }
958
+ function trimTrailingPunctuation(value) {
959
+ let text = value;
960
+ let trailing = "";
961
+ while (text && /[),.!?:;]+$/.test(text)) {
962
+ trailing = text.slice(-1) + trailing;
963
+ text = text.slice(0, -1);
964
+ }
965
+ return { text, trailing };
966
+ }
967
+ function renderLinkedText(text) {
968
+ if (!text) return null;
969
+ const matches = Array.from(text.matchAll(LINK_REGEX));
970
+ if (matches.length === 0) return text;
971
+ const nodes = [];
972
+ let lastIndex = 0;
973
+ matches.forEach((match, index) => {
974
+ const matchText = match[0];
975
+ const start = match.index ?? 0;
976
+ if (start > lastIndex) {
977
+ nodes.push(text.slice(lastIndex, start));
991
978
  }
992
- setAvailabilityStartDate((prev) => addDays(prev, availabilityDays));
993
- setSelectedDate(null);
994
- setSelectedTime(null);
995
- setSelectedEndTime(null);
996
- setBookingError(null);
997
- };
998
- const handlePrevMonth = () => {
999
- setCalendarMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() - 1, 1));
1000
- setSelectedDate(null);
1001
- setSelectedTime(null);
1002
- setSelectedEndTime(null);
1003
- setBookingError(null);
1004
- };
1005
- const handleNextMonth = () => {
1006
- if (maxAdvanceMonthStart) {
1007
- const nextMonth = new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() + 1, 1);
1008
- if (nextMonth > maxAdvanceMonthStart) return;
979
+ const { text: cleanText, trailing } = trimTrailingPunctuation(matchText);
980
+ if (cleanText) {
981
+ const href = buildHref(cleanText);
982
+ nodes.push(
983
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", children: cleanText }, `link-${start}-${index}`)
984
+ );
1009
985
  }
1010
- setCalendarMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() + 1, 1));
1011
- setSelectedDate(null);
1012
- setSelectedTime(null);
1013
- setSelectedEndTime(null);
1014
- setBookingError(null);
1015
- };
1016
- const handleMonthSelect = (value) => {
1017
- const [year, month] = value.split("-").map(Number);
1018
- if (!year || !month) return;
1019
- setCalendarMonth(new Date(year, month - 1, 1));
1020
- setSelectedDate(null);
1021
- setSelectedTime(null);
1022
- setSelectedEndTime(null);
1023
- setBookingError(null);
1024
- };
1025
- (0, import_react7.useEffect)(() => {
1026
- if (step !== "time") return;
1027
- if (!availability.data?.dates?.length) return;
1028
- const matches = selectedDate ? availability.data.dates.some((entry) => entry.date === selectedDate) : false;
1029
- if (matches) return;
1030
- setSelectedDate(availability.data.dates[0].date);
1031
- }, [step, availability.data, selectedDate]);
1032
- (0, import_react7.useEffect)(() => {
1033
- if (!selectedService) return;
1034
- if (datePickerMode === "calendar") {
1035
- const now = /* @__PURE__ */ new Date();
1036
- setCalendarMonth(new Date(now.getFullYear(), now.getMonth(), 1));
1037
- return;
986
+ if (trailing) {
987
+ nodes.push(trailing);
1038
988
  }
1039
- setAvailabilityStartDate((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
1040
- setAvailabilityDays(Math.max(1, availabilityDaysProp));
1041
- }, [selectedService?.id, selectedStaff?.id, availabilityDaysProp, datePickerMode]);
1042
- (0, import_react7.useEffect)(() => {
1043
- if (datePickerMode !== "calendar") return;
1044
- const start = getMonthStart(calendarMonth);
1045
- const end = getMonthEnd(calendarMonth);
1046
- const startString = start.toISOString().slice(0, 10);
1047
- const daysInMonth = end.getDate();
1048
- setAvailabilityStartDate(startString);
1049
- setAvailabilityDays(daysInMonth);
1050
- }, [calendarMonth, datePickerMode, getMonthEnd, getMonthStart]);
1051
- const handleBack = () => {
1052
- if (step === "service") return;
1053
- if (step === "staff") {
1054
- setStep("service");
1055
- } else if (step === "time") {
1056
- if (requiresStaff) {
1057
- setStep("staff");
1058
- } else {
1059
- setStep("service");
1060
- }
1061
- } else if (step === "details") {
1062
- setStep("time");
1063
- } else if (step === "done") {
1064
- setStep("details");
989
+ lastIndex = start + matchText.length;
990
+ });
991
+ if (lastIndex < text.length) {
992
+ nodes.push(text.slice(lastIndex));
993
+ }
994
+ return nodes;
995
+ }
996
+
997
+ // src/components/TeamMember.tsx
998
+ var import_jsx_runtime5 = require("react/jsx-runtime");
999
+ function TeamMember({
1000
+ member,
1001
+ className,
1002
+ showRole = true,
1003
+ showEmail = false,
1004
+ showBio = false,
1005
+ layout = "card",
1006
+ imagePosition = "left"
1007
+ }) {
1008
+ const initials = member.name?.split(" ").map((part) => part[0]).slice(0, 2).join("").toUpperCase();
1009
+ const avatar = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "idk-team__avatar", children: member.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: member.photo.url, alt: member.name }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: initials }) });
1010
+ if (layout === "profile") {
1011
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: ["idk-team__profile", className].filter(Boolean).join(" "), children: [
1012
+ imagePosition === "left" ? avatar : null,
1013
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "idk-team__info", children: [
1014
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "idk-card__title", children: member.name }),
1015
+ showRole ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__description", children: member.role || "Staff" }) : null,
1016
+ showBio && member.bio ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-team__bio", children: renderLinkedText(member.bio) }) : null,
1017
+ showEmail ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__meta", children: member.email }) : null
1018
+ ] }),
1019
+ imagePosition === "right" ? avatar : null
1020
+ ] });
1021
+ }
1022
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: ["idk-card", className].filter(Boolean).join(" "), children: [
1023
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "idk-team__avatar", children: member.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: member.photo.url, alt: member.name }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: initials }) }),
1024
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "idk-team__info", children: [
1025
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "idk-card__title", children: member.name }),
1026
+ showRole ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__description", children: member.role || "Staff" }) : null,
1027
+ showBio && member.bio ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-team__bio", children: renderLinkedText(member.bio) }) : null,
1028
+ showEmail ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "idk-card__meta", children: member.email }) : null
1029
+ ] })
1030
+ ] });
1031
+ }
1032
+
1033
+ // src/components/TeamGrid.tsx
1034
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1035
+ function TeamGrid({
1036
+ columns = 3,
1037
+ className,
1038
+ showRole,
1039
+ showEmail,
1040
+ showBio,
1041
+ layout = "card",
1042
+ imagePosition = "left",
1043
+ query,
1044
+ filter,
1045
+ sort,
1046
+ limit,
1047
+ emptyMessage = "No team members available.",
1048
+ loadingMessage = "Loading team..."
1049
+ }) {
1050
+ const { data, isLoading, error } = useTeam(query);
1051
+ if (isLoading) {
1052
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "idk-state", children: loadingMessage });
1053
+ }
1054
+ let members = data?.team ? [...data.team] : [];
1055
+ if (filter) {
1056
+ members = members.filter(filter);
1057
+ }
1058
+ if (sort) {
1059
+ members = members.sort(sort);
1060
+ }
1061
+ if (typeof limit === "number") {
1062
+ members = members.slice(0, Math.max(0, limit));
1063
+ }
1064
+ if (error || members.length === 0) {
1065
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "idk-state", children: emptyMessage });
1066
+ }
1067
+ const gridStyle = { gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))` };
1068
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1069
+ "div",
1070
+ {
1071
+ className: ["idk-team", className].filter(Boolean).join(" "),
1072
+ style: gridStyle,
1073
+ children: members.map((member) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1074
+ TeamMember,
1075
+ {
1076
+ member,
1077
+ showRole,
1078
+ showEmail,
1079
+ showBio,
1080
+ layout,
1081
+ imagePosition
1082
+ },
1083
+ member.id
1084
+ ))
1065
1085
  }
1066
- setBookingError(null);
1067
- scrollToTop();
1068
- };
1069
- const handleServiceContinue = () => {
1070
- if (!selectedService) return;
1071
- if (requiresStaff) {
1072
- setStep("staff");
1073
- } else {
1074
- setStep("time");
1086
+ );
1087
+ }
1088
+
1089
+ // src/components/ContactForm.tsx
1090
+ var import_react7 = require("react");
1091
+ var import_react_query7 = require("@tanstack/react-query");
1092
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1093
+ var DEFAULT_FIELDS = ["name", "email", "phone", "message"];
1094
+ function ContactForm({
1095
+ fields = DEFAULT_FIELDS,
1096
+ formType = "contact",
1097
+ submitLabel = "Send Message",
1098
+ className,
1099
+ onSuccess,
1100
+ onError
1101
+ }) {
1102
+ const config = usePlatformConfig();
1103
+ const [formState, setFormState] = (0, import_react7.useState)({
1104
+ name: "",
1105
+ email: "",
1106
+ phone: "",
1107
+ subject: "",
1108
+ message: ""
1109
+ });
1110
+ const mutation = (0, import_react_query7.useMutation)({
1111
+ mutationFn: (payload) => apiRequest(config, "/contact", {
1112
+ method: "POST",
1113
+ body: JSON.stringify(payload)
1114
+ }),
1115
+ onSuccess: () => {
1116
+ setFormState({ name: "", email: "", phone: "", subject: "", message: "" });
1117
+ onSuccess?.();
1118
+ },
1119
+ onError: (error) => {
1120
+ const message = error instanceof Error ? error.message : "Failed to submit form";
1121
+ onError?.(message);
1075
1122
  }
1076
- scrollToTop();
1077
- };
1078
- const handleStaffContinue = () => {
1079
- if (!canSkipStaff && !selectedStaff) return;
1080
- setStep("time");
1081
- scrollToTop();
1082
- };
1083
- const handleTimeContinue = () => {
1084
- if (!selectedTime || !selectedEndTime) return;
1085
- setStep("details");
1086
- scrollToTop();
1123
+ });
1124
+ const handleChange = (field, value) => {
1125
+ setFormState((prev) => ({ ...prev, [field]: value }));
1087
1126
  };
1088
- const handleSubmit = async (event) => {
1127
+ const handleSubmit = (event) => {
1089
1128
  event.preventDefault();
1090
- if (!selectedService || !selectedDate || !selectedTime || !selectedEndTime) return;
1091
- const start = parseDateTime(selectedTime, selectedDate);
1092
- const end = parseDateTime(selectedEndTime, selectedDate);
1093
- if (!start || !end) {
1094
- setBookingError("Please select a valid time slot.");
1095
- return;
1096
- }
1097
- try {
1098
- setBookingError(null);
1099
- const result = await createBooking.mutateAsync({
1100
- serviceId: selectedService.id,
1101
- staffId: selectedStaff?.id,
1102
- startTime: start.toISOString(),
1103
- endTime: end.toISOString(),
1104
- customerName: details.name,
1105
- customerEmail: details.email,
1106
- customerPhone: details.phone || void 0,
1107
- customerNotes: details.notes || void 0
1108
- });
1109
- setStep("done");
1110
- onSuccess?.(result?.booking);
1111
- scrollToTop();
1112
- } catch (error) {
1113
- setBookingError(
1114
- error instanceof Error ? error.message : "Unable to complete booking. Please try again."
1115
- );
1116
- }
1129
+ mutation.mutate({
1130
+ name: formState.name,
1131
+ email: formState.email,
1132
+ phone: formState.phone || void 0,
1133
+ subject: formState.subject || void 0,
1134
+ message: formState.message,
1135
+ formType
1136
+ });
1117
1137
  };
1118
- const isServicesLoading = services ? false : servicesLoading;
1119
- if (isServicesLoading) {
1138
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("form", { className: ["idk-form", className].filter(Boolean).join(" "), onSubmit: handleSubmit, children: [
1139
+ fields.includes("name") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
1140
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Name" }),
1141
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1142
+ "input",
1143
+ {
1144
+ value: formState.name,
1145
+ onChange: (event) => handleChange("name", event.target.value),
1146
+ required: true
1147
+ }
1148
+ )
1149
+ ] }),
1150
+ fields.includes("email") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
1151
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Email" }),
1152
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1153
+ "input",
1154
+ {
1155
+ type: "email",
1156
+ value: formState.email,
1157
+ onChange: (event) => handleChange("email", event.target.value),
1158
+ required: true
1159
+ }
1160
+ )
1161
+ ] }),
1162
+ fields.includes("phone") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
1163
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Phone" }),
1164
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1165
+ "input",
1166
+ {
1167
+ value: formState.phone,
1168
+ onChange: (event) => handleChange("phone", event.target.value)
1169
+ }
1170
+ )
1171
+ ] }),
1172
+ fields.includes("subject") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
1173
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Subject" }),
1174
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1175
+ "input",
1176
+ {
1177
+ value: formState.subject,
1178
+ onChange: (event) => handleChange("subject", event.target.value)
1179
+ }
1180
+ )
1181
+ ] }),
1182
+ fields.includes("message") && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "idk-form__field", children: [
1183
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Message" }),
1184
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1185
+ "textarea",
1186
+ {
1187
+ value: formState.message,
1188
+ onChange: (event) => handleChange("message", event.target.value),
1189
+ rows: 5,
1190
+ required: true
1191
+ }
1192
+ )
1193
+ ] }),
1194
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "submit", className: "idk-button", disabled: mutation.isPending, children: mutation.isPending ? "Sending..." : submitLabel }),
1195
+ mutation.isError ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-form__error", children: "Something went wrong. Please try again." }) : null,
1196
+ mutation.isSuccess ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "idk-form__success", children: "Thanks! We'll be in touch soon." }) : null
1197
+ ] });
1198
+ }
1199
+
1200
+ // src/components/BookingWidget.tsx
1201
+ var import_react8 = require("react");
1202
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1203
+ var parseDateOnly2 = (value) => {
1204
+ if (!value) return null;
1205
+ const normalized = value.includes("T") ? value.split("T")[0] : value;
1206
+ const parsed = /* @__PURE__ */ new Date(`${normalized}T00:00:00`);
1207
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
1208
+ };
1209
+ function BookingWidget({
1210
+ className,
1211
+ title = "Book an Appointment",
1212
+ subtitle,
1213
+ serviceSearchPlaceholder = "Search services",
1214
+ categoryLabel = "All categories",
1215
+ serviceListMaxHeight,
1216
+ serviceLayout = "list",
1217
+ serviceColumns = 2,
1218
+ showServicePagination,
1219
+ staffLayout = "grid",
1220
+ staffColumns = 2,
1221
+ showStaffPagination,
1222
+ timeLayout = "split",
1223
+ showFullyBookedLabel = true,
1224
+ calendarVariant = "default",
1225
+ enableServiceSearch = true,
1226
+ enableCategoryFilter = false,
1227
+ enableDatePaging = true,
1228
+ disabledWeekdays = [],
1229
+ ...hookProps
1230
+ }) {
1231
+ const widgetRef = (0, import_react8.useRef)(null);
1232
+ const bw = useBookingWidget({
1233
+ ...hookProps,
1234
+ enableServiceSearch,
1235
+ enableCategoryFilter,
1236
+ enableDatePaging,
1237
+ calendarVariant,
1238
+ disabledWeekdays,
1239
+ showFullyBookedLabel,
1240
+ widgetRef
1241
+ });
1242
+ const todayDate = (() => {
1243
+ const now = /* @__PURE__ */ new Date();
1244
+ now.setHours(0, 0, 0, 0);
1245
+ return now;
1246
+ })();
1247
+ const disabledWeekdaysSet = new Set(disabledWeekdays);
1248
+ const shouldPaginate = showServicePagination ?? bw.hasMoreServices;
1249
+ const shouldPaginateStaff = showStaffPagination ?? bw.hasMoreStaff;
1250
+ if (bw.isServicesLoading) {
1120
1251
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "Loading booking options..." });
1121
1252
  }
1122
- if (!servicesList.length) {
1253
+ if (!bw.servicesList.length) {
1123
1254
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No services available." });
1124
1255
  }
1125
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1126
- "div",
1127
- {
1128
- ref: widgetRef,
1129
- className: ["idk-booking", className].filter(Boolean).join(" "),
1130
- children: [
1131
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__header", children: [
1132
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__title-wrap", children: [
1133
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { className: "idk-booking__title", children: title }),
1134
- subtitle ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-booking__subtitle", children: subtitle }) : null
1135
- ] }),
1136
- step !== "service" && step !== "done" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "idk-link", onClick: handleBack, children: "Back" }) : null
1137
- ] }),
1138
- step !== "done" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__progress", children: [
1139
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { children: [
1140
- "Step ",
1141
- stepNumber,
1142
- " of ",
1143
- totalSteps
1144
- ] }),
1145
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__bar", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1146
- "div",
1147
- {
1148
- className: "idk-booking__bar-fill",
1149
- style: { width: `${stepNumber / totalSteps * 100}%` }
1150
- }
1151
- ) })
1152
- ] }) : null,
1153
- step === "service" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__step", children: [
1154
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Select a service" }),
1155
- enableServiceSearch || enableCategoryFilter ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__filters", children: [
1156
- enableServiceSearch ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1157
- "input",
1158
- {
1159
- type: "search",
1160
- value: serviceSearch,
1161
- onChange: (event) => setServiceSearch(event.target.value),
1162
- placeholder: serviceSearchPlaceholder
1163
- }
1164
- ) : null,
1165
- enableCategoryFilter && categories.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1166
- "select",
1256
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: widgetRef, className: ["idk-booking", className].filter(Boolean).join(" "), children: [
1257
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__header", children: [
1258
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__title-wrap", children: [
1259
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { className: "idk-booking__title", children: title }),
1260
+ subtitle ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-booking__subtitle", children: subtitle }) : null
1261
+ ] }),
1262
+ bw.step !== "service" && bw.step !== "done" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "idk-link", onClick: bw.goBack, children: "Back" }) : null
1263
+ ] }),
1264
+ bw.step !== "done" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__progress", children: [
1265
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { children: [
1266
+ "Step ",
1267
+ bw.stepNumber,
1268
+ " of ",
1269
+ bw.totalSteps
1270
+ ] }),
1271
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__bar", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1272
+ "div",
1273
+ {
1274
+ className: "idk-booking__bar-fill",
1275
+ style: { width: `${bw.stepNumber / bw.totalSteps * 100}%` }
1276
+ }
1277
+ ) })
1278
+ ] }) : null,
1279
+ bw.step === "service" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__step", children: [
1280
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Select a service" }),
1281
+ enableServiceSearch || enableCategoryFilter ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__filters", children: [
1282
+ enableServiceSearch ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1283
+ "input",
1284
+ {
1285
+ type: "search",
1286
+ value: bw.serviceSearch,
1287
+ onChange: (event) => bw.setServiceSearch(event.target.value),
1288
+ placeholder: serviceSearchPlaceholder
1289
+ }
1290
+ ) : null,
1291
+ enableCategoryFilter && bw.categories.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1292
+ "select",
1293
+ {
1294
+ value: bw.categoryFilter,
1295
+ onChange: (event) => bw.setCategoryFilter(event.target.value),
1296
+ children: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "all", children: categoryLabel }),
1298
+ bw.categories.map((category) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: category, children: category }, category))
1299
+ ]
1300
+ }
1301
+ ) : null
1302
+ ] }) : null,
1303
+ bw.filteredServices.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No services match your filters." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1304
+ "div",
1305
+ {
1306
+ className: [
1307
+ "idk-services",
1308
+ serviceLayout === "grid" ? "idk-services--grid" : "idk-services--list"
1309
+ ].join(" "),
1310
+ style: {
1311
+ ...serviceLayout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, serviceColumns)}, minmax(0, 1fr))` } : void 0,
1312
+ ...serviceListMaxHeight ? { maxHeight: serviceListMaxHeight, overflow: "auto" } : void 0
1313
+ },
1314
+ children: bw.pagedServices.map((service) => {
1315
+ const isSelected = bw.selectedService?.id === service.id;
1316
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1317
+ "button",
1167
1318
  {
1168
- value: categoryFilter,
1169
- onChange: (event) => setCategoryFilter(event.target.value),
1319
+ type: "button",
1320
+ className: ["idk-card", "idk-card--clickable", isSelected ? "is-active" : ""].join(" "),
1321
+ onClick: () => bw.selectService(service),
1170
1322
  children: [
1171
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "all", children: categoryLabel }),
1172
- categories.map((category) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: category, children: category }, category))
1323
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-card__header", children: [
1324
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__title", children: service.name }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "idk-card__aside", children: [
1326
+ isSelected ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__check", children: "\u2713" }) : null,
1327
+ typeof service.price === "number" && service.price > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "idk-card__price", children: [
1328
+ "$",
1329
+ (service.price / 100).toFixed(2)
1330
+ ] }) : null
1331
+ ] })
1332
+ ] }),
1333
+ service.description ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-card__description", children: service.description }) : null,
1334
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-card__meta", children: [
1335
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: bw.formatDuration(service.duration) }),
1336
+ service.category ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-pill", children: service.category }) : null
1337
+ ] })
1173
1338
  ]
1174
- }
1175
- ) : null
1176
- ] }) : null,
1177
- filteredServices.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No services match your filters." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1178
- "div",
1179
- {
1180
- className: [
1181
- "idk-services",
1182
- serviceLayout === "grid" ? "idk-services--grid" : "idk-services--list"
1183
- ].join(" "),
1184
- style: {
1185
- ...serviceLayout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, serviceColumns)}, minmax(0, 1fr))` } : void 0,
1186
- ...serviceListMaxHeight ? { maxHeight: serviceListMaxHeight, overflow: "auto" } : void 0
1187
1339
  },
1188
- children: pagedServices.map((service) => {
1189
- const isSelected = selectedService?.id === service.id;
1190
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1191
- "button",
1192
- {
1193
- type: "button",
1194
- className: ["idk-card", "idk-card--clickable", isSelected ? "is-active" : ""].join(" "),
1195
- onClick: () => {
1196
- setSelectedService(service);
1197
- resetSelections();
1198
- if (autoAdvanceOnSelect) {
1199
- if (requiresStaff) {
1200
- setStep("staff");
1201
- } else {
1202
- setStep("time");
1203
- }
1204
- scrollToTop();
1205
- }
1206
- },
1207
- children: [
1208
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-card__header", children: [
1209
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__title", children: service.name }),
1210
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "idk-card__aside", children: [
1211
- isSelected ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__check", children: "\u2713" }) : null,
1212
- typeof service.price === "number" && service.price > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "idk-card__price", children: [
1213
- "$",
1214
- (service.price / 100).toFixed(2)
1215
- ] }) : null
1216
- ] })
1217
- ] }),
1218
- service.description ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-card__description", children: service.description }) : null,
1219
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-card__meta", children: [
1220
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: formatDuration(service.duration) }),
1221
- service.category ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-pill", children: service.category }) : null
1222
- ] })
1223
- ]
1224
- },
1225
- service.id
1226
- );
1227
- })
1228
- }
1229
- ),
1230
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__actions", children: [
1231
- shouldPaginate ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1340
+ service.id
1341
+ );
1342
+ })
1343
+ }
1344
+ ),
1345
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__actions", children: [
1346
+ shouldPaginate ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1347
+ "button",
1348
+ {
1349
+ type: "button",
1350
+ className: "idk-button idk-button--ghost",
1351
+ onClick: bw.loadMoreServices,
1352
+ disabled: !bw.hasMoreServices,
1353
+ children: bw.hasMoreServices ? "Show more services" : "All services shown"
1354
+ }
1355
+ ) : null,
1356
+ bw.selectedService && !hookProps.autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "idk-button", onClick: bw.continueFromService, children: "Continue" }) : null
1357
+ ] })
1358
+ ] }),
1359
+ bw.step === "staff" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__step", children: [
1360
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Select a team member" }),
1361
+ bw.staffOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No team members available." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1362
+ "div",
1363
+ {
1364
+ className: [
1365
+ "idk-team",
1366
+ staffLayout === "grid" ? "idk-team--grid" : "idk-team--list"
1367
+ ].join(" "),
1368
+ style: staffLayout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, staffColumns)}, minmax(0, 1fr))` } : void 0,
1369
+ children: [
1370
+ (hookProps.showAnyStaffOption ?? true) && bw.canSkipStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1232
1371
  "button",
1233
1372
  {
1234
1373
  type: "button",
1235
- className: "idk-button idk-button--ghost",
1236
- onClick: () => setServicePage((prev) => prev + 1),
1237
- disabled: !hasMoreServices,
1238
- children: hasMoreServices ? "Show more services" : "All services shown"
1239
- }
1374
+ className: ["idk-card", "idk-card--clickable", !bw.selectedStaff ? "is-active" : ""].join(" "),
1375
+ onClick: () => bw.selectStaff(null),
1376
+ children: [
1377
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-card__header", children: [
1378
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__title", children: "Any Available" }),
1379
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__badge", children: "Recommended" })
1380
+ ] }),
1381
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-card__description", children: "We'll match you with the first available stylist." })
1382
+ ]
1383
+ },
1384
+ "any-staff"
1240
1385
  ) : null,
1241
- selectedService && !autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "idk-button", onClick: handleServiceContinue, children: "Continue" }) : null
1242
- ] })
1243
- ] }),
1244
- step === "staff" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__step", children: [
1245
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Select a team member" }),
1246
- staffOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No team members available." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1247
- "div",
1248
- {
1249
- className: [
1250
- "idk-team",
1251
- staffLayout === "grid" ? "idk-team--grid" : "idk-team--list"
1252
- ].join(" "),
1253
- style: staffLayout === "grid" ? { gridTemplateColumns: `repeat(${Math.max(1, staffColumns)}, minmax(0, 1fr))` } : void 0,
1254
- children: [
1255
- showAnyStaffOption && canSkipStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1386
+ bw.pagedStaff.map((staff) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1387
+ "button",
1388
+ {
1389
+ type: "button",
1390
+ className: ["idk-card", "idk-card--clickable", bw.selectedStaff?.id === staff.id ? "is-active" : ""].join(" "),
1391
+ onClick: () => bw.selectStaff(staff),
1392
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-team__card", children: [
1393
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-team__avatar", children: staff.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: staff.photo.url, alt: staff.name }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: staff.name.split(" ").map((part) => part[0]).slice(0, 2).join("").toUpperCase() }) }),
1394
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1395
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__title", children: staff.name }),
1396
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__meta", children: staff.role || "Staff" }),
1397
+ staff.bio ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-card__description", children: renderLinkedText(staff.bio) }) : null
1398
+ ] })
1399
+ ] })
1400
+ },
1401
+ staff.id
1402
+ ))
1403
+ ]
1404
+ }
1405
+ ),
1406
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__actions", children: [
1407
+ shouldPaginateStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1408
+ "button",
1409
+ {
1410
+ type: "button",
1411
+ className: "idk-button idk-button--ghost",
1412
+ onClick: bw.loadMoreStaff,
1413
+ disabled: !bw.hasMoreStaff && bw.staffPage === 1,
1414
+ children: bw.hasMoreStaff ? "Show more staff" : bw.staffPage > 1 ? "Show fewer staff" : "All staff shown"
1415
+ }
1416
+ ) : null,
1417
+ bw.canSkipStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1418
+ "button",
1419
+ {
1420
+ type: "button",
1421
+ className: "idk-link",
1422
+ onClick: () => {
1423
+ bw.selectStaff(null);
1424
+ bw.continueFromStaff();
1425
+ },
1426
+ children: "Continue without selecting"
1427
+ }
1428
+ ) : null,
1429
+ !hookProps.autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1430
+ "button",
1431
+ {
1432
+ type: "button",
1433
+ className: "idk-button",
1434
+ onClick: bw.continueFromStaff,
1435
+ disabled: !bw.canSkipStaff && !bw.selectedStaff,
1436
+ children: "Continue"
1437
+ }
1438
+ ) : null
1439
+ ] })
1440
+ ] }),
1441
+ bw.step === "time" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__step", children: [
1442
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Select a time" }),
1443
+ bw.isAvailabilityLoading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "Loading availability..." }) : bw.availabilityDates.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: ["idk-availability", timeLayout === "split" ? "idk-availability--split" : ""].join(" "), children: [
1444
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__dates", children: (hookProps.datePickerMode ?? "list") === "calendar" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1445
+ "div",
1446
+ {
1447
+ className: [
1448
+ "idk-calendar",
1449
+ calendarVariant === "compact" ? "idk-calendar--compact" : ""
1450
+ ].filter(Boolean).join(" "),
1451
+ children: [
1452
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__header", children: calendarVariant === "compact" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-calendar__header idk-calendar__header--compact", children: [
1453
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1256
1454
  "button",
1257
1455
  {
1258
1456
  type: "button",
1259
- className: ["idk-card", "idk-card--clickable", !selectedStaff ? "is-active" : ""].join(" "),
1260
- onClick: () => {
1261
- setSelectedStaff(null);
1262
- setSelectedDate(null);
1263
- setSelectedTime(null);
1264
- setSelectedEndTime(null);
1265
- if (autoAdvanceOnSelect) {
1266
- setStep("time");
1267
- scrollToTop();
1268
- }
1269
- },
1270
- children: [
1271
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-card__header", children: [
1272
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__title", children: "Any Available" }),
1273
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__badge", children: "Recommended" })
1274
- ] }),
1275
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-card__description", children: "We'll match you with the first available stylist." })
1276
- ]
1277
- },
1278
- "any-staff"
1279
- ) : null,
1280
- pagedStaff.map((staff) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1457
+ className: "idk-calendar__nav-btn",
1458
+ onClick: bw.handlePrevMonth,
1459
+ disabled: bw.calendarMonth <= new Date(todayDate.getFullYear(), todayDate.getMonth(), 1),
1460
+ "aria-label": "Previous month",
1461
+ children: "<"
1462
+ }
1463
+ ),
1464
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1465
+ "select",
1466
+ {
1467
+ className: "idk-calendar__month-select",
1468
+ value: `${bw.calendarMonth.getFullYear()}-${String(bw.calendarMonth.getMonth() + 1).padStart(2, "0")}`,
1469
+ onChange: (event) => bw.handleMonthSelect(event.target.value),
1470
+ children: bw.monthOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: option.value, children: option.label }, option.value))
1471
+ }
1472
+ ),
1473
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1281
1474
  "button",
1282
1475
  {
1283
1476
  type: "button",
1284
- className: ["idk-card", "idk-card--clickable", selectedStaff?.id === staff.id ? "is-active" : ""].join(" "),
1285
- onClick: () => {
1286
- setSelectedStaff(staff);
1287
- setSelectedDate(null);
1288
- setSelectedTime(null);
1289
- setSelectedEndTime(null);
1290
- setBookingError(null);
1291
- if (autoAdvanceOnSelect) {
1292
- setStep("time");
1293
- scrollToTop();
1294
- }
1295
- },
1296
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-team__card", children: [
1297
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-team__avatar", children: staff.photo?.url ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: staff.photo.url, alt: staff.name }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: staff.name.split(" ").map((part) => part[0]).slice(0, 2).join("").toUpperCase() }) }),
1298
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1299
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__title", children: staff.name }),
1300
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-card__meta", children: staff.role || "Staff" }),
1301
- staff.bio ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-card__description", children: renderLinkedText(staff.bio) }) : null
1302
- ] })
1303
- ] })
1304
- },
1305
- staff.id
1306
- ))
1307
- ]
1308
- }
1309
- ),
1310
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__actions", children: [
1311
- shouldPaginateStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1312
- "button",
1313
- {
1314
- type: "button",
1315
- className: "idk-button idk-button--ghost",
1316
- onClick: () => setStaffPage((prev) => hasMoreStaff ? prev + 1 : 1),
1317
- disabled: !hasMoreStaff && staffPage === 1,
1318
- children: hasMoreStaff ? "Show more staff" : staffPage > 1 ? "Show fewer staff" : "All staff shown"
1319
- }
1320
- ) : null,
1321
- canSkipStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1322
- "button",
1323
- {
1324
- type: "button",
1325
- className: "idk-link",
1326
- onClick: () => {
1327
- setSelectedStaff(null);
1328
- setStep("time");
1329
- scrollToTop();
1330
- },
1331
- children: "Continue without selecting"
1332
- }
1333
- ) : null,
1334
- !autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1335
- "button",
1336
- {
1337
- type: "button",
1338
- className: "idk-button",
1339
- onClick: handleStaffContinue,
1340
- disabled: !canSkipStaff && !selectedStaff,
1341
- children: "Continue"
1342
- }
1343
- ) : null
1344
- ] })
1345
- ] }),
1346
- step === "time" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__step", children: [
1347
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Select a time" }),
1348
- availability.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "Loading availability..." }) : availability.data ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: ["idk-availability", timeLayout === "split" ? "idk-availability--split" : ""].join(" "), children: [
1349
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__dates", children: datePickerMode === "calendar" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1350
- "div",
1351
- {
1352
- className: [
1353
- "idk-calendar",
1354
- calendarVariant === "compact" ? "idk-calendar--compact" : ""
1355
- ].filter(Boolean).join(" "),
1356
- children: [
1357
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__header", children: calendarVariant === "compact" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-calendar__header idk-calendar__header--compact", children: [
1358
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1359
- "button",
1360
- {
1361
- type: "button",
1362
- className: "idk-calendar__nav-btn",
1363
- onClick: handlePrevMonth,
1364
- disabled: calendarMonth <= getMonthStart(todayDate),
1365
- "aria-label": "Previous month",
1366
- children: "<"
1367
- }
1368
- ),
1369
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1370
- "select",
1371
- {
1372
- className: "idk-calendar__month-select",
1373
- value: `${calendarMonth.getFullYear()}-${String(calendarMonth.getMonth() + 1).padStart(2, "0")}`,
1374
- onChange: (event) => handleMonthSelect(event.target.value),
1375
- children: monthOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: option.value, children: option.label }, option.value))
1376
- }
1377
- ),
1378
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1379
- "button",
1380
- {
1381
- type: "button",
1382
- className: "idk-calendar__nav-btn",
1383
- onClick: handleNextMonth,
1384
- disabled: Boolean(maxAdvanceMonthStart && calendarMonth >= maxAdvanceMonthStart),
1385
- "aria-label": "Next month",
1386
- children: ">"
1387
- }
1388
- )
1389
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1390
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1391
- "button",
1392
- {
1393
- type: "button",
1394
- className: "idk-button idk-button--ghost",
1395
- onClick: handlePrevMonth,
1396
- disabled: calendarMonth <= getMonthStart(todayDate),
1397
- children: "Prev"
1398
- }
1399
- ),
1400
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__title", children: new Intl.DateTimeFormat("en-US", {
1401
- month: "long",
1402
- year: "numeric"
1403
- }).format(calendarMonth) }),
1404
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1405
- "button",
1406
- {
1407
- type: "button",
1408
- className: "idk-button idk-button--ghost",
1409
- onClick: handleNextMonth,
1410
- disabled: Boolean(maxAdvanceMonthStart && calendarMonth >= maxAdvanceMonthStart),
1411
- children: "Next"
1412
- }
1413
- )
1414
- ] }) }),
1415
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__weekdays", children: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((label) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: label }, label)) }),
1416
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__grid", children: calendarDays.map((day) => {
1417
- const entry = availabilityByDate.get(day.date);
1418
- const hasAvailable = entry?.slots?.some((slot) => slot.available) ?? false;
1419
- const dateValue = parseDateOnly(day.date);
1420
- const isPast = dateValue ? dateValue < todayDate : false;
1421
- const isBeyondMax = maxAdvanceDate ? dateValue ? dateValue > maxAdvanceDate : false : false;
1422
- const weekday = dateValue ? dateValue.getDay() : null;
1423
- const isDisabledWeekday = weekday !== null ? disabledWeekdaysSet.has(weekday) : false;
1424
- const isBlocked = Boolean(entry) && !hasAvailable;
1425
- const isDisabled = !entry || isPast || isBeyondMax || isBlocked || isDisabledWeekday;
1426
- const isActive = selectedDate === day.date;
1427
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1428
- "button",
1429
- {
1430
- type: "button",
1431
- className: [
1432
- "idk-calendar__day",
1433
- day.isCurrentMonth ? "" : "is-muted",
1434
- hasAvailable ? "is-available" : "",
1435
- isBlocked ? "is-blocked" : "",
1436
- isDisabledWeekday ? "is-disabled" : "",
1437
- isActive ? "is-active" : ""
1438
- ].filter(Boolean).join(" "),
1439
- disabled: isDisabled,
1440
- onClick: () => {
1441
- setSelectedDate(day.date);
1442
- setSelectedTime(null);
1443
- setSelectedEndTime(null);
1444
- setBookingError(null);
1445
- },
1446
- children: [
1447
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: day.date.split("-")[2] }),
1448
- showFullyBookedLabel && isBlocked && day.isCurrentMonth ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-calendar__label", children: calendarVariant === "compact" ? "\u2022" : "Fully booked" }) : null
1449
- ]
1450
- },
1451
- day.date
1452
- );
1453
- }) }),
1454
- showFullyBookedLabel && calendarVariant === "compact" && hasBlockedInCalendarView ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-calendar__legend", children: [
1455
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-calendar__legend-dot", children: "\u2022" }),
1456
- " Fully booked"
1457
- ] }) : null
1458
- ]
1459
- }
1460
- ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1461
- enableDatePaging ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-availability__nav", children: [
1477
+ className: "idk-calendar__nav-btn",
1478
+ onClick: bw.handleNextMonth,
1479
+ disabled: Boolean(bw.maxAdvanceMonthStart && bw.calendarMonth >= bw.maxAdvanceMonthStart),
1480
+ "aria-label": "Next month",
1481
+ children: ">"
1482
+ }
1483
+ )
1484
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1462
1485
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1463
1486
  "button",
1464
1487
  {
1465
1488
  type: "button",
1466
1489
  className: "idk-button idk-button--ghost",
1467
- onClick: handlePrevDates,
1468
- disabled: availabilityStartDate <= todayString,
1469
- children: "Previous"
1490
+ onClick: bw.handlePrevMonth,
1491
+ disabled: bw.calendarMonth <= new Date(todayDate.getFullYear(), todayDate.getMonth(), 1),
1492
+ children: "Prev"
1470
1493
  }
1471
1494
  ),
1495
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__title", children: new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(bw.calendarMonth) }),
1472
1496
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1473
1497
  "button",
1474
1498
  {
1475
1499
  type: "button",
1476
1500
  className: "idk-button idk-button--ghost",
1477
- onClick: handleNextDates,
1501
+ onClick: bw.handleNextMonth,
1502
+ disabled: Boolean(bw.maxAdvanceMonthStart && bw.calendarMonth >= bw.maxAdvanceMonthStart),
1478
1503
  children: "Next"
1479
1504
  }
1480
1505
  )
1481
- ] }) : null,
1482
- availability.data.dates.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1483
- "button",
1484
- {
1485
- type: "button",
1486
- className: entry.date === selectedDate ? "is-active" : void 0,
1487
- onClick: () => {
1488
- setSelectedDate(entry.date);
1489
- setSelectedTime(null);
1490
- setSelectedEndTime(null);
1491
- setBookingError(null);
1492
- },
1493
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-date-chip__day", children: formatDateLabel(entry.date) })
1494
- },
1495
- entry.date
1496
- ))
1497
- ] }) }),
1498
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-availability__panel", children: [
1499
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__panel-header", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1500
- /* @__PURE__ */ (0, import_jsx_runtime8.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" }),
1501
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__panel-subtitle", children: timeZoneLabel })
1502
1506
  ] }) }),
1503
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__slots", children: (() => {
1504
- const activeEntry = selectedDate ? availability.data.dates.find((entry) => entry.date === selectedDate) : availability.data.dates[0];
1505
- if (!activeEntry) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "Select a date to see times." });
1506
- const availableSlots = (activeEntry.slots || []).filter((slot) => slot.available).sort((a, b) => {
1507
- const aTime = parseDateTime(a.time, activeEntry.date)?.getTime() ?? Number.POSITIVE_INFINITY;
1508
- const bTime = parseDateTime(b.time, activeEntry.date)?.getTime() ?? Number.POSITIVE_INFINITY;
1509
- if (aTime === bTime) {
1510
- const aEnd = parseDateTime(a.endTime, activeEntry.date)?.getTime() ?? Number.POSITIVE_INFINITY;
1511
- const bEnd = parseDateTime(b.endTime, activeEntry.date)?.getTime() ?? Number.POSITIVE_INFINITY;
1512
- return aEnd - bEnd;
1513
- }
1514
- return aTime - bTime;
1515
- });
1516
- const slots = availableSlots.filter(
1517
- (slot, index) => index === 0 || slot.time !== availableSlots[index - 1].time || slot.endTime !== availableSlots[index - 1].endTime
1518
- );
1519
- if (slots.length === 0) {
1520
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No available times for this date." });
1521
- }
1522
- return slots.map((slot, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1507
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__weekdays", children: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((label) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: label }, label)) }),
1508
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-calendar__grid", children: bw.calendarDays.map((day) => {
1509
+ const entry = bw.availabilityByDate.get(day.date);
1510
+ const hasAvailable = entry?.slots?.some((slot) => slot.available) ?? false;
1511
+ const dateValue = parseDateOnly2(day.date);
1512
+ const isPast = dateValue ? dateValue < todayDate : false;
1513
+ const isBeyondMax = bw.maxAdvanceDate ? dateValue ? dateValue > bw.maxAdvanceDate : false : false;
1514
+ const weekday = dateValue ? dateValue.getDay() : null;
1515
+ const isDisabledWeekday = weekday !== null ? disabledWeekdaysSet.has(weekday) : false;
1516
+ const isBlocked = Boolean(entry) && !hasAvailable;
1517
+ const isDisabled = !entry || isPast || isBeyondMax || isBlocked || isDisabledWeekday;
1518
+ const isActive = bw.selectedDate === day.date;
1519
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1523
1520
  "button",
1524
1521
  {
1525
1522
  type: "button",
1526
- className: slot.time === selectedTime && slot.endTime === selectedEndTime ? "is-active" : void 0,
1527
- onClick: () => {
1528
- const date = selectedDate || activeEntry.date;
1529
- setSelectedDate(date || null);
1530
- setSelectedTime(slot.time);
1531
- setSelectedEndTime(slot.endTime);
1532
- setBookingError(null);
1533
- if (autoAdvanceOnSelect) {
1534
- setStep("details");
1535
- scrollToTop();
1536
- }
1537
- },
1538
- children: formatTimeLabel(slot.time)
1523
+ className: [
1524
+ "idk-calendar__day",
1525
+ day.isCurrentMonth ? "" : "is-muted",
1526
+ hasAvailable ? "is-available" : "",
1527
+ isBlocked ? "is-blocked" : "",
1528
+ isDisabledWeekday ? "is-disabled" : "",
1529
+ isActive ? "is-active" : ""
1530
+ ].filter(Boolean).join(" "),
1531
+ disabled: isDisabled,
1532
+ onClick: () => bw.selectDate(day.date),
1533
+ children: [
1534
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: day.date.split("-")[2] }),
1535
+ showFullyBookedLabel && isBlocked && day.isCurrentMonth ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-calendar__label", children: calendarVariant === "compact" ? "\u2022" : "Fully booked" }) : null
1536
+ ]
1539
1537
  },
1540
- `${activeEntry.date}-${slot.time}-${slot.endTime}-${index}`
1541
- ));
1542
- })() })
1543
- ] })
1544
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No availability found." }),
1545
- !autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__actions", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1546
- "button",
1547
- {
1548
- type: "button",
1549
- className: "idk-button",
1550
- onClick: handleTimeContinue,
1551
- disabled: !selectedTime || !selectedEndTime,
1552
- children: "Continue"
1553
- }
1554
- ) }) : null
1555
- ] }),
1556
- step === "details" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("form", { className: "idk-form", onSubmit: handleSubmit, children: [
1557
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Your details" }),
1558
- bookingError ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__error", children: bookingError }) : null,
1559
- selectedService && selectedTime ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__summary", children: [
1560
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1561
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "Service" }),
1562
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: selectedService.name })
1563
- ] }),
1564
- selectedStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1565
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "Staff" }),
1566
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: selectedStaff.name })
1567
- ] }) : null,
1568
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1569
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "When" }),
1570
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
1571
- formatDateHeading(selectedDate || ""),
1572
- " \xB7 ",
1573
- formatTimeLabel(selectedTime)
1574
- ] })
1575
- ] }),
1576
- selectedService.price ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1577
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "Price" }),
1578
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
1579
- "$",
1580
- (selectedService.price / 100).toFixed(2)
1581
- ] })
1582
- ] }) : null
1583
- ] }) : null,
1584
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1585
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Name" }),
1586
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1587
- "input",
1588
- {
1589
- value: details.name,
1590
- onChange: (event) => setDetails((prev) => ({ ...prev, name: event.target.value })),
1591
- required: true
1592
- }
1593
- )
1594
- ] }),
1595
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1596
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Email" }),
1597
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1598
- "input",
1599
- {
1600
- type: "email",
1601
- value: details.email,
1602
- onChange: (event) => setDetails((prev) => ({ ...prev, email: event.target.value })),
1603
- required: true
1604
- }
1605
- )
1606
- ] }),
1607
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1608
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Phone" }),
1538
+ day.date
1539
+ );
1540
+ }) }),
1541
+ showFullyBookedLabel && calendarVariant === "compact" && bw.hasBlockedInCalendarView ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-calendar__legend", children: [
1542
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-calendar__legend-dot", children: "\u2022" }),
1543
+ " Fully booked"
1544
+ ] }) : null
1545
+ ]
1546
+ }
1547
+ ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1548
+ enableDatePaging ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-availability__nav", children: [
1609
1549
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1610
- "input",
1550
+ "button",
1611
1551
  {
1612
- value: details.phone,
1613
- onChange: (event) => setDetails((prev) => ({ ...prev, phone: event.target.value }))
1552
+ type: "button",
1553
+ className: "idk-button idk-button--ghost",
1554
+ onClick: bw.handlePrevDates,
1555
+ disabled: bw.availabilityStartDate <= (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1556
+ children: "Previous"
1614
1557
  }
1615
- )
1616
- ] }),
1617
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1618
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Notes" }),
1558
+ ),
1619
1559
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1620
- "textarea",
1560
+ "button",
1621
1561
  {
1622
- rows: 4,
1623
- value: details.notes,
1624
- onChange: (event) => setDetails((prev) => ({ ...prev, notes: event.target.value }))
1562
+ type: "button",
1563
+ className: "idk-button idk-button--ghost",
1564
+ onClick: bw.handleNextDates,
1565
+ children: "Next"
1625
1566
  }
1626
1567
  )
1627
- ] }),
1628
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "submit", className: "idk-button", disabled: createBooking.isPending, children: createBooking.isPending ? "Booking..." : "Confirm booking" })
1629
- ] }),
1630
- step === "done" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__done", children: [
1631
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__done-title", children: "Booking confirmed!" }),
1632
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-booking__done-text", children: "We'll send a confirmation email shortly with your appointment details." }),
1633
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1568
+ ] }) : null,
1569
+ bw.availabilityDates.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1634
1570
  "button",
1635
1571
  {
1636
1572
  type: "button",
1637
- className: "idk-link",
1638
- onClick: () => {
1639
- setStep("service");
1640
- setSelectedService(null);
1641
- resetSelections();
1642
- setDetails({ name: "", email: "", phone: "", notes: "" });
1643
- scrollToTop();
1644
- },
1645
- children: "Book another appointment"
1573
+ className: entry.date === bw.selectedDate ? "is-active" : void 0,
1574
+ onClick: () => bw.selectDate(entry.date),
1575
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-date-chip__day", children: bw.formatDateLabel(entry.date) })
1576
+ },
1577
+ entry.date
1578
+ ))
1579
+ ] }) }),
1580
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-availability__panel", children: [
1581
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__panel-header", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1582
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__panel-title", children: bw.selectedDate ? bw.formatDateHeading(bw.selectedDate) : bw.availabilityDates[0]?.date ? bw.formatDateHeading(bw.availabilityDates[0].date) : "Select a date" }),
1583
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__panel-subtitle", children: bw.timeZoneLabel })
1584
+ ] }) }),
1585
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-availability__slots", children: (() => {
1586
+ const activeEntry = bw.selectedDate ? bw.availabilityDates.find((entry) => entry.date === bw.selectedDate) : bw.availabilityDates[0];
1587
+ if (!activeEntry) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "Select a date to see times." });
1588
+ const availableSlots = (activeEntry.slots || []).filter((slot) => slot.available).sort((a, b) => {
1589
+ const aTime = new Date(a.time).getTime() || Number.POSITIVE_INFINITY;
1590
+ const bTime = new Date(b.time).getTime() || Number.POSITIVE_INFINITY;
1591
+ if (aTime === bTime) {
1592
+ const aEnd = new Date(a.endTime).getTime() || Number.POSITIVE_INFINITY;
1593
+ const bEnd = new Date(b.endTime).getTime() || Number.POSITIVE_INFINITY;
1594
+ return aEnd - bEnd;
1595
+ }
1596
+ return aTime - bTime;
1597
+ });
1598
+ const slots = availableSlots.filter(
1599
+ (slot, index) => index === 0 || slot.time !== availableSlots[index - 1].time || slot.endTime !== availableSlots[index - 1].endTime
1600
+ );
1601
+ if (slots.length === 0) {
1602
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No available times for this date." });
1646
1603
  }
1647
- )
1604
+ return slots.map((slot, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1605
+ "button",
1606
+ {
1607
+ type: "button",
1608
+ className: slot.time === bw.selectedTime && slot.endTime === bw.selectedEndTime ? "is-active" : void 0,
1609
+ onClick: () => {
1610
+ const date = bw.selectedDate || activeEntry.date;
1611
+ if (!bw.selectedDate) bw.selectDate(date);
1612
+ bw.selectTime(slot.time, slot.endTime);
1613
+ },
1614
+ children: bw.formatTimeLabel(slot.time)
1615
+ },
1616
+ `${activeEntry.date}-${slot.time}-${slot.endTime}-${index}`
1617
+ ));
1618
+ })() })
1648
1619
  ] })
1649
- ]
1650
- }
1651
- );
1620
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-state", children: "No availability found." }),
1621
+ !hookProps.autoAdvanceOnSelect ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__actions", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1622
+ "button",
1623
+ {
1624
+ type: "button",
1625
+ className: "idk-button",
1626
+ onClick: bw.continueFromTime,
1627
+ disabled: !bw.selectedTime || !bw.selectedEndTime,
1628
+ children: "Continue"
1629
+ }
1630
+ ) }) : null
1631
+ ] }),
1632
+ bw.step === "details" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("form", { className: "idk-form", onSubmit: bw.submitBooking, children: [
1633
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h4", { children: "Your details" }),
1634
+ bw.bookingError ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__error", children: bw.bookingError }) : null,
1635
+ bw.selectedService && bw.selectedTime ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__summary", children: [
1636
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1637
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "Service" }),
1638
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: bw.selectedService.name })
1639
+ ] }),
1640
+ bw.selectedStaff ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1641
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "Staff" }),
1642
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: bw.selectedStaff.name })
1643
+ ] }) : null,
1644
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1645
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "When" }),
1646
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
1647
+ bw.formatDateHeading(bw.selectedDate || ""),
1648
+ " \xB7 ",
1649
+ bw.formatTimeLabel(bw.selectedTime)
1650
+ ] })
1651
+ ] }),
1652
+ bw.selectedService.price ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1653
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "idk-booking__summary-label", children: "Price" }),
1654
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
1655
+ "$",
1656
+ (bw.selectedService.price / 100).toFixed(2)
1657
+ ] })
1658
+ ] }) : null
1659
+ ] }) : null,
1660
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1661
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Name" }),
1662
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1663
+ "input",
1664
+ {
1665
+ value: bw.details.name,
1666
+ onChange: (event) => bw.setDetails((prev) => ({ ...prev, name: event.target.value })),
1667
+ required: true
1668
+ }
1669
+ )
1670
+ ] }),
1671
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1672
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Email" }),
1673
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1674
+ "input",
1675
+ {
1676
+ type: "email",
1677
+ value: bw.details.email,
1678
+ onChange: (event) => bw.setDetails((prev) => ({ ...prev, email: event.target.value })),
1679
+ required: true
1680
+ }
1681
+ )
1682
+ ] }),
1683
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1684
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Phone" }),
1685
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1686
+ "input",
1687
+ {
1688
+ value: bw.details.phone,
1689
+ onChange: (event) => bw.setDetails((prev) => ({ ...prev, phone: event.target.value }))
1690
+ }
1691
+ )
1692
+ ] }),
1693
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "idk-form__field", children: [
1694
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Notes" }),
1695
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1696
+ "textarea",
1697
+ {
1698
+ rows: 4,
1699
+ value: bw.details.notes,
1700
+ onChange: (event) => bw.setDetails((prev) => ({ ...prev, notes: event.target.value }))
1701
+ }
1702
+ )
1703
+ ] }),
1704
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "submit", className: "idk-button", disabled: bw.isSubmitting, children: bw.isSubmitting ? "Booking..." : "Confirm booking" })
1705
+ ] }),
1706
+ bw.step === "done" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "idk-booking__done", children: [
1707
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "idk-booking__done-title", children: "Booking confirmed!" }),
1708
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "idk-booking__done-text", children: "We'll send a confirmation email shortly with your appointment details." }),
1709
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "idk-link", onClick: bw.reset, children: "Book another appointment" })
1710
+ ] })
1711
+ ] });
1652
1712
  }
1653
1713
 
1654
1714
  // src/components/AvailabilityPicker.tsx
1655
- var import_react8 = require("react");
1715
+ var import_react9 = require("react");
1656
1716
  var import_jsx_runtime9 = require("react/jsx-runtime");
1657
1717
  function AvailabilityPicker({
1658
1718
  serviceId,
@@ -1662,8 +1722,8 @@ function AvailabilityPicker({
1662
1722
  className,
1663
1723
  onSelect
1664
1724
  }) {
1665
- const [selectedDate, setSelectedDate] = (0, import_react8.useState)(date || null);
1666
- const [selectedTime, setSelectedTime] = (0, import_react8.useState)(null);
1725
+ const [selectedDate, setSelectedDate] = (0, import_react9.useState)(date || null);
1726
+ const [selectedTime, setSelectedTime] = (0, import_react9.useState)(null);
1667
1727
  const { data, isLoading } = useAvailability({
1668
1728
  serviceId,
1669
1729
  staffId,
@@ -1819,6 +1879,7 @@ function TimePicker({
1819
1879
  TimePicker,
1820
1880
  useAvailability,
1821
1881
  useBookingLookup,
1882
+ useBookingWidget,
1822
1883
  useCancelBooking,
1823
1884
  useCreateBooking,
1824
1885
  useServices,