UNPKG

booking-ui

Version:

React booking widget component for ZvenBook booking system

1,027 lines (1,018 loc) 40.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.tsx var index_exports = {}; __export(index_exports, { BookingSteps: () => BookingSteps, BookingStepsWidget: () => BookingStepsWidget, Button: () => Button, ProviderPicker: () => ProviderPicker, ServicePicker: () => ServicePicker, SlotList: () => SlotList, bookingStepsVariants: () => bookingStepsVariants, cn: () => cn, defaultLabels: () => defaultLabels, swedishLabels: () => swedishLabels }); module.exports = __toCommonJS(index_exports); // src/ServicePicker.tsx var import_jsx_runtime = require("react/jsx-runtime"); function ServicePicker({ services, value, onChange, className }) { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "select", { value, onChange: (e) => onChange(e.target.value), className: "border rounded px-3 py-2 bg-white dark:bg-zinc-900 " + (className ?? ""), children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "Select service" }), services.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: s.id, children: s.name }, s.id)) ] } ); } // src/ProviderPicker.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); function ProviderPicker({ providers, value, onChange, className }) { return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "select", { value, onChange: (e) => onChange(e.target.value), className: "border rounded px-3 py-2 bg-white dark:bg-zinc-900 " + (className ?? ""), children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: "Select provider" }), providers.map((p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: p.id, children: p.name }, p.id)) ] } ); } // src/SlotList.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); function SlotList({ slots, onSelect, className }) { if (!slots.length) { return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className, children: "No slots in range." }); } return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "grid gap-2 sm:grid-cols-2 " + (className ?? ""), children: slots.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)( "button", { className: "px-3 py-2 rounded-md bg-black text-white", onClick: () => onSelect(s), children: [ new Date(s.start).toLocaleTimeString(), " \u2013", " ", new Date(s.end).toLocaleTimeString() ] }, s.start )) }); } // src/BookingSteps.tsx var React = __toESM(require("react"), 1); var import_class_variance_authority = require("class-variance-authority"); // src/lib/cn.ts var import_clsx = require("clsx"); var import_tailwind_merge = require("tailwind-merge"); function cn(...inputs) { return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs)); } // src/BookingSteps.tsx var import_booking_api_sdk = require("booking-api-sdk"); var import_react_fontawesome = require("@fortawesome/react-fontawesome"); var import_pro_regular_svg_icons = require("@fortawesome/pro-regular-svg-icons"); var import_jsx_runtime4 = require("react/jsx-runtime"); var bookingStepsVariants = (0, import_class_variance_authority.cva)("relative flex", { variants: { layout: { horizontal: "flex-row items-stretch", vertical: "flex-col", wizard: "flex-col" }, spacing: { compact: "gap-2", normal: "gap-4", cozy: "gap-6" }, radius: { none: "rounded-none", sm: "rounded-sm", md: "rounded-md", lg: "rounded-lg", full: "rounded-full" }, color: { accent: "bg-accent text-accent-foreground", primary: "bg-primary text-primary-foreground", muted: "bg-muted text-muted-foreground", transparent: "" } }, defaultVariants: { layout: "horizontal", spacing: "normal", radius: "md", color: "accent" } }); function BookingSteps({ layout, spacing, radius, color, className, children }) { return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { className: cn( bookingStepsVariants({ layout, spacing, radius, color }), className ), children } ); } function InlineCalendar({ value, onChange, daysWithAvailability = [], weekStartsOn = "sunday", dayLabels, className }) { const today = /* @__PURE__ */ new Date(); const initial = value ? new Date(value) : today; const [cursor, setCursor] = React.useState( new Date(initial.getFullYear(), initial.getMonth(), 1) ); const year = cursor.getFullYear(); const month = cursor.getMonth(); const firstDay = new Date(year, month, 1); const startWeekday = firstDay.getDay(); const adjustedStartWeekday = weekStartsOn === "monday" ? startWeekday === 0 ? 6 : startWeekday - 1 : startWeekday; const daysInMonth = new Date(year, month + 1, 0).getDate(); const weeks = []; let day = 1 - adjustedStartWeekday; while (day <= daysInMonth) { const week = []; for (let i = 0; i < 7; i++) { if (day < 1 || day > daysInMonth) { week.push(null); } else { week.push(new Date(year, month, day)); } day++; } weeks.push(week); } function fmt(d) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${dd}`; } const selectedStr = value; const dayLabelsArray = weekStartsOn === "monday" ? [ dayLabels.monday, dayLabels.tuesday, dayLabels.wednesday, dayLabels.thursday, dayLabels.friday, dayLabels.saturday, dayLabels.sunday ] : [ dayLabels.sunday, dayLabels.monday, dayLabels.tuesday, dayLabels.wednesday, dayLabels.thursday, dayLabels.friday, dayLabels.saturday ]; return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cn("inline-block border rounded-md p-3", className), children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { type: "button", className: "px-2 py-1 text-sm border rounded", onClick: () => setCursor(new Date(year, month - 1, 1)), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_fontawesome.FontAwesomeIcon, { icon: import_pro_regular_svg_icons.faChevronLeft }) } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-sm font-medium", children: cursor.toLocaleString(void 0, { month: "long", year: "numeric" }) }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { type: "button", className: "px-2 py-1 text-sm border rounded", onClick: () => setCursor(new Date(year, month + 1, 1)), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_fontawesome.FontAwesomeIcon, { icon: import_pro_regular_svg_icons.faChevronRight }) } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "grid grid-cols-7 text-xs text-muted-foreground mb-1", children: dayLabelsArray.map((d) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-center", children: d }, d)) }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "grid grid-cols-7", children: weeks.map( (week, wi) => week.map((d, di) => { if (!d) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-8" }, `${wi}-${di}`); const valueStr = fmt(d); const isSelected = selectedStr === valueStr; const hasAvailability = daysWithAvailability.includes(valueStr); return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "button", { type: "button", onClick: () => onChange(valueStr), className: cn( "h-8 rounded text-sm border hover:bg-muted relative", isSelected && "bg-accent text-accent-foreground border-accent" ), children: [ d.getDate(), hasAvailability && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "absolute top-2 right-2 -translate-x-1/2 w-1 h-1 bg-green-500 rounded-full" }) ] }, `${wi}-${di}` ); }) ) }) ] }); } var defaultLabels = { stepService: "Service", stepDateTime: "Date & time", stepDetails: "Your details", chooseService: "Choose a service", selectDate: "Select date", loadingTimes: "Loading available times...", noSlotsAvailable: "No slots available.", dayLabels: { sunday: "Su", monday: "Mo", tuesday: "Tu", wednesday: "We", thursday: "Th", friday: "Fr", saturday: "Sa" }, yourDetails: "Your details", firstNamePlaceholder: "First name", lastNamePlaceholder: "Last name", emailPlaceholder: "Email", phonePlaceholder: "Phone (optional)", back: "Back", next: "Next", bookAppointment: "Book appointment", booking: "Booking\u2026", bookingConfirmed: "Booking confirmed", changeService: "Service selected \u2022 change", changeDate: "{date} \u2022 change", changeTime: "{time} \u2022 change" }; var swedishLabels = { stepService: "Tj\xE4nst", stepDateTime: "Datum & tid", stepDetails: "Dina uppgifter", chooseService: "V\xE4lj en tj\xE4nst", selectDate: "V\xE4lj datum", loadingTimes: "Laddar tillg\xE4ngliga tider...", noSlotsAvailable: "Inga lediga tider.", dayLabels: { sunday: "S\xF6", monday: "M\xE5", tuesday: "Ti", wednesday: "On", thursday: "To", friday: "Fr", saturday: "L\xF6" }, yourDetails: "Dina uppgifter", firstNamePlaceholder: "F\xF6rnamn", lastNamePlaceholder: "Efternamn", emailPlaceholder: "E-post", phonePlaceholder: "Telefon (valfritt)", back: "Tillbaka", next: "N\xE4sta", bookAppointment: "Boka tid", booking: "Bokar\u2026", bookingConfirmed: "Bokning bekr\xE4ftad", changeService: "Tj\xE4nst vald \u2022 \xE4ndra", changeDate: "{date} \u2022 \xE4ndra", changeTime: "{time} \u2022 \xE4ndra" }; function BookingStepsWidget({ baseUrl, tenantId, className, layout, spacing, radius, color, weekStartsOn = "sunday", labels = defaultLabels, styles }) { const sdk = React.useMemo(() => (0, import_booking_api_sdk.createSdk)({ baseUrl }), [baseUrl]); const [services, setServices] = React.useState([]); const [serviceId, setServiceId] = React.useState(""); const [date, setDate] = React.useState(""); const [slots, setSlots] = React.useState([]); const [slot, setSlot] = React.useState(null); const [contact, setContact] = React.useState({ firstName: "", lastName: "", email: "" }); const [step, setStep] = React.useState(0); const [submitting, setSubmitting] = React.useState(false); const [success, setSuccess] = React.useState(null); const [error, setError] = React.useState(null); const [daysWithAvailability, setDaysWithAvailability] = React.useState([]); const [loadingSlots, setLoadingSlots] = React.useState(false); const [loadingServices, setLoadingServices] = React.useState(true); const [validationErrors, setValidationErrors] = React.useState({}); React.useEffect(() => { let cancelled = false; (async () => { setLoadingServices(true); try { const { services: services2 } = await sdk.listServices({ tenantId }); if (!cancelled) { setServices(services2); } } catch (e) { if (!cancelled) setError(e?.message ?? "Failed to load data"); } finally { if (!cancelled) setLoadingServices(false); } })(); return () => { cancelled = true; }; }, [sdk, tenantId]); React.useEffect(() => { if (serviceId && step === 0) setStep(1); }, [serviceId, step]); React.useEffect(() => { if (slot && step < 2) setStep(2); }, [slot, step]); React.useEffect(() => { (async () => { if (!serviceId) { setDaysWithAvailability([]); return; } const today = /* @__PURE__ */ new Date(); const start = new Date(today.getFullYear(), today.getMonth(), 1); const end = new Date(today.getFullYear(), today.getMonth() + 2, 0); try { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; const { serviceAvailability } = await sdk.serviceAvailability({ tenantId, start: start.toISOString(), end: end.toISOString(), timezone: tz }); const current = (serviceAvailability || []).find( (s) => s.serviceId === serviceId ); const datesSet = /* @__PURE__ */ new Set(); (current?.slots || []).forEach((s) => { const slotDate = new Date(s.start); const yyyy = slotDate.getFullYear(); const mm = String(slotDate.getMonth() + 1).padStart(2, "0"); const dd = String(slotDate.getDate()).padStart(2, "0"); datesSet.add(`${yyyy}-${mm}-${dd}`); }); setDaysWithAvailability(Array.from(datesSet)); } catch (e) { setDaysWithAvailability([]); } })(); }, [sdk, tenantId, serviceId]); React.useEffect(() => { (async () => { if (!serviceId || !date) { setSlots([]); setLoadingSlots(false); return; } setLoadingSlots(true); const day = new Date(date); const start = new Date(day); start.setHours(0, 0, 0, 0); const end = new Date(day); end.setHours(23, 59, 59, 999); try { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; const { serviceAvailability } = await sdk.serviceAvailability({ tenantId, start: start.toISOString(), end: end.toISOString(), timezone: tz }); const current = (serviceAvailability || []).find( (s) => s.serviceId === serviceId ); const daySlots = (current?.slots || []).map((s) => ({ start: s.start, end: s.end, providerId: s.providerId, providerName: s.providerName })); setSlots(daySlots); } catch (e) { setError(e?.message ?? "Failed to load availability"); } finally { setLoadingSlots(false); } })(); }, [sdk, tenantId, serviceId, date]); async function resolveProviderForSlot(desired) { if (slot?.providerId) return slot.providerId; try { const { providers } = await sdk.listProviders({ tenantId }); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; for (const p of providers) { try { const { slots: slots2 } = await sdk.availability({ tenantId, serviceId, providerId: p._id, start: desired.start, end: desired.end, timezone: tz }); const found = slots2.some( (s) => s.start === desired.start && s.end === desired.end ); if (found) return p._id; } catch (_) { } } } catch (_) { } return null; } function isContactValid() { if (!contact.firstName.trim() || !contact.lastName.trim() || !contact.email.trim()) { return false; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(contact.email); } function validateContact() { const errors = {}; if (!contact.firstName.trim()) { errors.firstName = "First name is required"; } if (!contact.lastName.trim()) { errors.lastName = "Last name is required"; } if (!contact.email.trim()) { errors.email = "Email is required"; } else { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(contact.email)) { errors.email = "Please enter a valid email address"; } } setValidationErrors(errors); return Object.keys(errors).length === 0; } async function submitBooking() { if (!slot || !serviceId) return; if (!validateContact()) { return; } setSubmitting(true); setError(null); try { const providerId = await resolveProviderForSlot({ start: slot.start, end: slot.end }); if (!providerId) throw new Error("No provider available for this time"); const res = await sdk.createBooking({ tenantId, serviceId, providerId, start: slot.start, end: slot.end, contactFirstName: contact.firstName, contactLastName: contact.lastName, contactEmail: contact.email, contactPhone: contact.phone }); if ("ok" in res || "_id" in res) { setSuccess(labels.bookingConfirmed); } } catch (e) { setError(e?.message ?? "Booking failed"); } finally { setSubmitting(false); } } if (success) { return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { className: cn( bookingStepsVariants({ layout, spacing, radius, color }), className ), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "p-8 rounded-lg bg-white dark:bg-zinc-900 border text-center animate-in fade-in duration-500", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mb-4 animate-in zoom-in duration-700", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "svg", { className: "w-8 h-8 text-green-600 dark:text-green-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" } ) } ) }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2", children: success }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-muted-foreground", children: "You will receive a confirmation email shortly." }) ] }) } ); } const cssVars = React.useMemo(() => { const vars = {}; if (styles?.primaryColor) vars["--booking-primary"] = styles.primaryColor; if (styles?.accentColor) vars["--booking-accent"] = styles.accentColor; if (styles?.borderColor) vars["--booking-border"] = styles.borderColor; if (styles?.textColor) vars["--booking-text"] = styles.textColor; if (styles?.backgroundColor) vars["--booking-bg"] = styles.backgroundColor; return vars; }, [styles]); return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "div", { className: cn( bookingStepsVariants({ layout, spacing, radius, color }), className ), style: cssVars, children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn("w-full", styles?.stepperClassName), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mb-6 flex items-center", children: [labels.stepService, labels.stepDateTime, labels.stepDetails].map( (label, i) => { const isActive = step === i; const isDone = step > i; return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(React.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col items-center gap-2", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { className: cn( "w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border-2 transition-all duration-300", isActive && "bg-accent text-accent-foreground border-accent scale-100", !isActive && !isDone && "bg-muted text-foreground/70 border-border", isDone && "bg-primary text-primary-foreground border-primary" ), children: isDone ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_fontawesome.FontAwesomeIcon, { icon: import_pro_regular_svg_icons.faCheck }) : i + 1 } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { className: cn( "text-xs whitespace-nowrap transition-all duration-300", isActive ? "font-semibold text-foreground" : "text-muted-foreground" ), children: label } ) ] }), i < 2 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-1 h-0.5 mx-2 bg-border relative overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { className: cn( "absolute inset-0 bg-white transition-all duration-500 ease-in-out", isDone ? "translate-x-0" : "-translate-x-[105%]" ) } ) }) ] }, label); } ) }) }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "w-full mb-2 flex flex-wrap items-center gap-2 text-xs", children: [ serviceId && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { type: "button", className: "px-2 py-1 rounded border bg-muted hover:bg-muted/70", onClick: () => { setServiceId(""); setDate(""); setSlots([]); setSlot(null); setStep(0); }, children: labels.changeService } ), date && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { type: "button", className: "px-2 py-1 rounded border bg-muted hover:bg-muted/70", onClick: () => { setSlot(null); setStep(1); }, children: labels.changeDate.replace( "{date}", new Date(date).toLocaleDateString() ) } ), slot && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { type: "button", className: "px-2 py-1 rounded border bg-muted hover:bg-muted/70", onClick: () => { setSlot(null); setStep(1); }, children: labels.changeTime.replace( "{time}", `${new Date(slot.start).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}\u2013${new Date(slot.end).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` ) } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "w-full border rounded-lg p-4 bg-white dark:bg-zinc-900", children: [ step === 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-3", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "text-sm font-medium", children: labels.chooseService }), loadingServices ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col items-center justify-center py-12", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "svg", { className: "animate-spin h-8 w-8 text-gray-400", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" } ) ] } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-muted-foreground mt-3", children: "Loading services..." }) ] }) : services.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "svg", { className: "h-12 w-12 text-gray-300 dark:text-gray-600 mb-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" } ) } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "No services available" }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-muted-foreground mt-1", children: "Please check back later or contact support." }) ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: services.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "button", { onClick: () => { setServiceId(s._id); setStep(1); }, className: cn( "border rounded-md p-3 text-left hover:border-ring transition-colors", serviceId === s._id && "ring-2 ring-accent", styles?.serviceCardClassName ), children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "font-medium", children: s.name }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "text-xs text-muted-foreground", children: [ s.durationMin, " min" ] }) ] }, s._id )) }) ] }), step === 1 && serviceId && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-3 mt-4", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "text-sm font-medium", children: labels.selectDate }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( InlineCalendar, { value: date, onChange: (d) => { setDate(d); setSlot(null); }, daysWithAvailability, weekStartsOn, dayLabels: labels.dayLabels, className: styles?.calendarClassName } ), date && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-4 mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: [ "Available times for", " ", /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-accent", children: new Date(date).toLocaleDateString(void 0, { weekday: "long", year: "numeric", month: "long", day: "numeric" }) }) ] }) }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "grid gap-2 sm:grid-cols-2", children: loadingSlots ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "svg", { className: "animate-spin h-4 w-4", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" } ) ] } ), labels.loadingTimes ] }) : slots.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-sm text-muted-foreground", children: labels.noSlotsAvailable }) : slots.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "button", { className: cn( "px-3 py-2 rounded-md bg-black text-white", slot?.start === s.start && "ring-2 ring-accent", styles?.timeSlotClassName ), onClick: () => { setSlot(s); setStep(2); }, children: [ new Date(s.start).toLocaleTimeString(), " \u2013", " ", new Date(s.end).toLocaleTimeString() ] }, s.start )) }) ] }) ] }), step === 2 && slot && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-3 mt-4", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "text-sm font-medium", children: labels.yourDetails }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { placeholder: labels.firstNamePlaceholder, className: cn( "w-full border rounded px-3 py-2 bg-white dark:bg-zinc-900 transition-colors", validationErrors.firstName ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300 dark:border-gray-600", styles?.inputClassName ), value: contact.firstName, onChange: (e) => { setContact({ ...contact, firstName: e.target.value }); setValidationErrors({ ...validationErrors, firstName: void 0 }); } } ), validationErrors.firstName && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-red-600 mt-1", children: validationErrors.firstName }) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { placeholder: labels.lastNamePlaceholder, className: cn( "w-full border rounded px-3 py-2 bg-white dark:bg-zinc-900 transition-colors", validationErrors.lastName ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300 dark:border-gray-600", styles?.inputClassName ), value: contact.lastName, onChange: (e) => { setContact({ ...contact, lastName: e.target.value }); setValidationErrors({ ...validationErrors, lastName: void 0 }); } } ), validationErrors.lastName && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-red-600 mt-1", children: validationErrors.lastName }) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { type: "email", placeholder: labels.emailPlaceholder, className: cn( "w-full border rounded px-3 py-2 bg-white dark:bg-zinc-900 transition-colors", validationErrors.email ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300 dark:border-gray-600", styles?.inputClassName ), value: contact.email, onChange: (e) => { setContact({ ...contact, email: e.target.value }); setValidationErrors({ ...validationErrors, email: void 0 }); } } ), validationErrors.email && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-red-600 mt-1", children: validationErrors.email }) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { placeholder: labels.phonePlaceholder, className: cn( "w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2 bg-white dark:bg-zinc-900", styles?.inputClassName ), value: contact.phone ?? "", onChange: (e) => setContact({ ...contact, phone: e.target.value }) } ) }), error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "p-3 rounded-md bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) }) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mt-4 flex items-center justify-between", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { className: cn( "px-3 py-2 rounded-md border text-sm", styles?.buttonClassName ), onClick: () => { if (step === 2) { setSlot(null); setStep(1); } else if (step === 1) { setDate(""); setSlots([]); setServiceId(""); setStep(0); } }, disabled: step === 0, children: labels.back } ), step < 2 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { className: cn( "px-4 py-2 rounded-md bg-accent text-accent-foreground text-sm disabled:opacity-50", styles?.buttonClassName ), onClick: () => setStep((s) => Math.min(2, s + 1)), disabled: step === 0 && !serviceId || step === 1 && !slot, children: labels.next } ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "button", { disabled: submitting || !slot || !isContactValid(), onClick: submitBooking, className: cn( "px-4 py-2 rounded-md bg-primary border border-primary text-primary-foreground disabled:opacity-50 shadow hover:opacity-90", styles?.submitButtonClassName || styles?.buttonClassName ), children: submitting ? labels.booking : labels.bookAppointment } ) ] }) ] }) ] } ); } // src/index.tsx var import_jsx_runtime5 = require("react/jsx-runtime"); function Button(props) { return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "button", { ...props, className: "px-3 py-2 rounded-md bg-black text-white " + (props.className ?? "") } ); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { BookingSteps, BookingStepsWidget, Button, ProviderPicker, ServicePicker, SlotList, bookingStepsVariants, cn, defaultLabels, swedishLabels });