@resira/ui 0.3.2 → 0.4.0
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 +227 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -5
- package/dist/index.d.ts +60 -5
- package/dist/index.js +227 -79
- package/dist/index.js.map +1 -1
- package/dist/styles.css +232 -0
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -234,6 +234,28 @@ interface ResiraClassNames {
|
|
|
234
234
|
/** Price preview panel. */
|
|
235
235
|
pricePreview?: string;
|
|
236
236
|
}
|
|
237
|
+
/** Pre-filled selection data for deeplink integration. */
|
|
238
|
+
interface DeeplinkSelection {
|
|
239
|
+
/** Pre-selected product/service ID. */
|
|
240
|
+
productId?: string;
|
|
241
|
+
/** Pre-selected date (YYYY-MM-DD). */
|
|
242
|
+
date?: string;
|
|
243
|
+
/** Pre-selected party size. */
|
|
244
|
+
partySize?: number;
|
|
245
|
+
/** Pre-selected duration in minutes. */
|
|
246
|
+
duration?: number;
|
|
247
|
+
}
|
|
248
|
+
/** Pre-filled guest data for deeplink integration. */
|
|
249
|
+
interface DeeplinkGuest {
|
|
250
|
+
/** Guest name. */
|
|
251
|
+
name?: string;
|
|
252
|
+
/** Guest email. */
|
|
253
|
+
email?: string;
|
|
254
|
+
/** Guest phone. */
|
|
255
|
+
phone?: string;
|
|
256
|
+
/** Notes. */
|
|
257
|
+
notes?: string;
|
|
258
|
+
}
|
|
237
259
|
/** Configuration passed to ResiraProvider. */
|
|
238
260
|
interface ResiraProviderConfig {
|
|
239
261
|
/** Theme overrides. */
|
|
@@ -270,6 +292,23 @@ interface ResiraProviderConfig {
|
|
|
270
292
|
showRemainingSpots?: boolean;
|
|
271
293
|
/** Percentage of total to charge upfront (0-100). Default 100. */
|
|
272
294
|
depositPercent?: number;
|
|
295
|
+
/** Whether to show the step indicator bar. @default true */
|
|
296
|
+
showStepIndicator?: boolean;
|
|
297
|
+
/** Pre-fill booking data for deeplink integration. */
|
|
298
|
+
deeplink?: DeeplinkSelection;
|
|
299
|
+
/** Pre-fill guest information. */
|
|
300
|
+
deeplinkGuest?: DeeplinkGuest;
|
|
301
|
+
/** Called when the booking step changes. */
|
|
302
|
+
onStepChange?: (step: BookingStep) => void;
|
|
303
|
+
/** Called when a booking is successfully completed. */
|
|
304
|
+
onBookingComplete?: (data: {
|
|
305
|
+
reservationId?: string;
|
|
306
|
+
product?: Product;
|
|
307
|
+
selection: BookingSelection;
|
|
308
|
+
guest: GuestFormValues;
|
|
309
|
+
}) => void;
|
|
310
|
+
/** Called when an error occurs during the booking flow. */
|
|
311
|
+
onError?: (code: string, message: string) => void;
|
|
273
312
|
}
|
|
274
313
|
/** Props for the ResiraProvider component. */
|
|
275
314
|
interface ResiraProviderProps {
|
|
@@ -336,6 +375,23 @@ interface ResiraContextValue {
|
|
|
336
375
|
groupServicesByCategory: boolean;
|
|
337
376
|
/** Custom render function for service cards. */
|
|
338
377
|
renderServiceCard?: (product: Product, selected: boolean) => React.ReactNode;
|
|
378
|
+
/** Whether the step indicator bar is visible. */
|
|
379
|
+
showStepIndicator: boolean;
|
|
380
|
+
/** Pre-fill booking data for deeplink integration. */
|
|
381
|
+
deeplink?: DeeplinkSelection;
|
|
382
|
+
/** Pre-fill guest information. */
|
|
383
|
+
deeplinkGuest?: DeeplinkGuest;
|
|
384
|
+
/** Called when the booking step changes. */
|
|
385
|
+
onStepChange?: (step: BookingStep) => void;
|
|
386
|
+
/** Called when a booking is successfully completed. */
|
|
387
|
+
onBookingComplete?: (data: {
|
|
388
|
+
reservationId?: string;
|
|
389
|
+
product?: Product;
|
|
390
|
+
selection: BookingSelection;
|
|
391
|
+
guest: GuestFormValues;
|
|
392
|
+
}) => void;
|
|
393
|
+
/** Called when an error occurs during the booking flow. */
|
|
394
|
+
onError?: (code: string, message: string) => void;
|
|
339
395
|
}
|
|
340
396
|
/** Steps in the booking flow. */
|
|
341
397
|
type BookingStep = "resource" | "availability" | "details" | "terms" | "payment" | "confirmation";
|
|
@@ -465,14 +521,13 @@ interface ProductSelectorProps {
|
|
|
465
521
|
onSelect: (product: Product) => void;
|
|
466
522
|
loading?: boolean;
|
|
467
523
|
error?: string | null;
|
|
468
|
-
/** Override layout from provider. */
|
|
469
524
|
layout?: ServiceLayout;
|
|
470
|
-
/** Override visible count from provider. */
|
|
471
525
|
visibleCount?: number;
|
|
472
|
-
/** Override category grouping from provider. @default true */
|
|
473
526
|
groupByCategory?: boolean;
|
|
527
|
+
/** Pre-select a specific category to show (skip category tiles). */
|
|
528
|
+
initialCategory?: string;
|
|
474
529
|
}
|
|
475
|
-
declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
|
|
530
|
+
declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, initialCategory, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
|
|
476
531
|
|
|
477
532
|
/**
|
|
478
533
|
* Validate the guest form. Returns an errors object (empty = valid).
|
|
@@ -743,4 +798,4 @@ declare function TagIcon({ size, className }: IconProps): react_jsx_runtime.JSX.
|
|
|
743
798
|
declare function CubeIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
|
|
744
799
|
declare function ViewfinderIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
|
|
745
800
|
|
|
746
|
-
export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
|
|
801
|
+
export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, type DeeplinkGuest, type DeeplinkSelection, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
|
package/dist/index.d.ts
CHANGED
|
@@ -234,6 +234,28 @@ interface ResiraClassNames {
|
|
|
234
234
|
/** Price preview panel. */
|
|
235
235
|
pricePreview?: string;
|
|
236
236
|
}
|
|
237
|
+
/** Pre-filled selection data for deeplink integration. */
|
|
238
|
+
interface DeeplinkSelection {
|
|
239
|
+
/** Pre-selected product/service ID. */
|
|
240
|
+
productId?: string;
|
|
241
|
+
/** Pre-selected date (YYYY-MM-DD). */
|
|
242
|
+
date?: string;
|
|
243
|
+
/** Pre-selected party size. */
|
|
244
|
+
partySize?: number;
|
|
245
|
+
/** Pre-selected duration in minutes. */
|
|
246
|
+
duration?: number;
|
|
247
|
+
}
|
|
248
|
+
/** Pre-filled guest data for deeplink integration. */
|
|
249
|
+
interface DeeplinkGuest {
|
|
250
|
+
/** Guest name. */
|
|
251
|
+
name?: string;
|
|
252
|
+
/** Guest email. */
|
|
253
|
+
email?: string;
|
|
254
|
+
/** Guest phone. */
|
|
255
|
+
phone?: string;
|
|
256
|
+
/** Notes. */
|
|
257
|
+
notes?: string;
|
|
258
|
+
}
|
|
237
259
|
/** Configuration passed to ResiraProvider. */
|
|
238
260
|
interface ResiraProviderConfig {
|
|
239
261
|
/** Theme overrides. */
|
|
@@ -270,6 +292,23 @@ interface ResiraProviderConfig {
|
|
|
270
292
|
showRemainingSpots?: boolean;
|
|
271
293
|
/** Percentage of total to charge upfront (0-100). Default 100. */
|
|
272
294
|
depositPercent?: number;
|
|
295
|
+
/** Whether to show the step indicator bar. @default true */
|
|
296
|
+
showStepIndicator?: boolean;
|
|
297
|
+
/** Pre-fill booking data for deeplink integration. */
|
|
298
|
+
deeplink?: DeeplinkSelection;
|
|
299
|
+
/** Pre-fill guest information. */
|
|
300
|
+
deeplinkGuest?: DeeplinkGuest;
|
|
301
|
+
/** Called when the booking step changes. */
|
|
302
|
+
onStepChange?: (step: BookingStep) => void;
|
|
303
|
+
/** Called when a booking is successfully completed. */
|
|
304
|
+
onBookingComplete?: (data: {
|
|
305
|
+
reservationId?: string;
|
|
306
|
+
product?: Product;
|
|
307
|
+
selection: BookingSelection;
|
|
308
|
+
guest: GuestFormValues;
|
|
309
|
+
}) => void;
|
|
310
|
+
/** Called when an error occurs during the booking flow. */
|
|
311
|
+
onError?: (code: string, message: string) => void;
|
|
273
312
|
}
|
|
274
313
|
/** Props for the ResiraProvider component. */
|
|
275
314
|
interface ResiraProviderProps {
|
|
@@ -336,6 +375,23 @@ interface ResiraContextValue {
|
|
|
336
375
|
groupServicesByCategory: boolean;
|
|
337
376
|
/** Custom render function for service cards. */
|
|
338
377
|
renderServiceCard?: (product: Product, selected: boolean) => React.ReactNode;
|
|
378
|
+
/** Whether the step indicator bar is visible. */
|
|
379
|
+
showStepIndicator: boolean;
|
|
380
|
+
/** Pre-fill booking data for deeplink integration. */
|
|
381
|
+
deeplink?: DeeplinkSelection;
|
|
382
|
+
/** Pre-fill guest information. */
|
|
383
|
+
deeplinkGuest?: DeeplinkGuest;
|
|
384
|
+
/** Called when the booking step changes. */
|
|
385
|
+
onStepChange?: (step: BookingStep) => void;
|
|
386
|
+
/** Called when a booking is successfully completed. */
|
|
387
|
+
onBookingComplete?: (data: {
|
|
388
|
+
reservationId?: string;
|
|
389
|
+
product?: Product;
|
|
390
|
+
selection: BookingSelection;
|
|
391
|
+
guest: GuestFormValues;
|
|
392
|
+
}) => void;
|
|
393
|
+
/** Called when an error occurs during the booking flow. */
|
|
394
|
+
onError?: (code: string, message: string) => void;
|
|
339
395
|
}
|
|
340
396
|
/** Steps in the booking flow. */
|
|
341
397
|
type BookingStep = "resource" | "availability" | "details" | "terms" | "payment" | "confirmation";
|
|
@@ -465,14 +521,13 @@ interface ProductSelectorProps {
|
|
|
465
521
|
onSelect: (product: Product) => void;
|
|
466
522
|
loading?: boolean;
|
|
467
523
|
error?: string | null;
|
|
468
|
-
/** Override layout from provider. */
|
|
469
524
|
layout?: ServiceLayout;
|
|
470
|
-
/** Override visible count from provider. */
|
|
471
525
|
visibleCount?: number;
|
|
472
|
-
/** Override category grouping from provider. @default true */
|
|
473
526
|
groupByCategory?: boolean;
|
|
527
|
+
/** Pre-select a specific category to show (skip category tiles). */
|
|
528
|
+
initialCategory?: string;
|
|
474
529
|
}
|
|
475
|
-
declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
|
|
530
|
+
declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, initialCategory, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
|
|
476
531
|
|
|
477
532
|
/**
|
|
478
533
|
* Validate the guest form. Returns an errors object (empty = valid).
|
|
@@ -743,4 +798,4 @@ declare function TagIcon({ size, className }: IconProps): react_jsx_runtime.JSX.
|
|
|
743
798
|
declare function CubeIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
|
|
744
799
|
declare function ViewfinderIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
|
|
745
800
|
|
|
746
|
-
export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
|
|
801
|
+
export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, type DeeplinkGuest, type DeeplinkSelection, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
|
package/dist/index.js
CHANGED
|
@@ -222,6 +222,12 @@ function ResiraProvider({
|
|
|
222
222
|
const visibleServiceCount = config?.visibleServiceCount ?? 4;
|
|
223
223
|
const groupServicesByCategory = config?.groupServicesByCategory ?? true;
|
|
224
224
|
const renderServiceCard = config?.renderServiceCard;
|
|
225
|
+
const showStepIndicator = config?.showStepIndicator ?? true;
|
|
226
|
+
const deeplink = config?.deeplink;
|
|
227
|
+
const deeplinkGuest = config?.deeplinkGuest;
|
|
228
|
+
const onStepChange = config?.onStepChange;
|
|
229
|
+
const onBookingComplete = config?.onBookingComplete;
|
|
230
|
+
const onError = config?.onError;
|
|
225
231
|
const cssVars = useMemo(() => themeToCSS(theme), [theme]);
|
|
226
232
|
const value = useMemo(
|
|
227
233
|
() => ({
|
|
@@ -248,9 +254,15 @@ function ResiraProvider({
|
|
|
248
254
|
serviceLayout,
|
|
249
255
|
visibleServiceCount,
|
|
250
256
|
groupServicesByCategory,
|
|
251
|
-
renderServiceCard
|
|
257
|
+
renderServiceCard,
|
|
258
|
+
showStepIndicator,
|
|
259
|
+
deeplink,
|
|
260
|
+
deeplinkGuest,
|
|
261
|
+
onStepChange,
|
|
262
|
+
onBookingComplete,
|
|
263
|
+
onError
|
|
252
264
|
}),
|
|
253
|
-
[client, resourceId, activeResourceId, setActiveResourceId, catalogMode, allowMultiSelect, domain, theme, locale, domainConfig, stripePublishableKey, termsText, waiverText, showWaiver, showTerms, showRemainingSpots, depositPercent, refundPolicy, onClose, classNames, serviceLayout, visibleServiceCount, groupServicesByCategory, renderServiceCard]
|
|
265
|
+
[client, resourceId, activeResourceId, setActiveResourceId, catalogMode, allowMultiSelect, domain, theme, locale, domainConfig, stripePublishableKey, termsText, waiverText, showWaiver, showTerms, showRemainingSpots, depositPercent, refundPolicy, onClose, classNames, serviceLayout, visibleServiceCount, groupServicesByCategory, renderServiceCard, showStepIndicator, deeplink, deeplinkGuest, onStepChange, onBookingComplete, onError]
|
|
254
266
|
);
|
|
255
267
|
return /* @__PURE__ */ jsx(ResiraContext.Provider, { value, children: /* @__PURE__ */ jsx("div", { className: "resira-root", style: cssVars, children }) });
|
|
256
268
|
}
|
|
@@ -1630,39 +1642,90 @@ function formatCategoryLabel(resourceType) {
|
|
|
1630
1642
|
return resourceType.trim().split(/[_\-\s]+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()).join(" ");
|
|
1631
1643
|
}
|
|
1632
1644
|
function groupProductsByCategory(products, resources) {
|
|
1633
|
-
const
|
|
1634
|
-
resources.map((resource) => [resource.id, resource
|
|
1645
|
+
const resourceById = new Map(
|
|
1646
|
+
resources.map((resource) => [resource.id, resource])
|
|
1635
1647
|
);
|
|
1636
1648
|
const groups = /* @__PURE__ */ new Map();
|
|
1637
1649
|
products.forEach((product) => {
|
|
1638
|
-
|
|
1650
|
+
let categoryResource;
|
|
1651
|
+
const categoryType = product.equipmentIds.map((equipmentId) => {
|
|
1652
|
+
const res = resourceById.get(equipmentId);
|
|
1653
|
+
if (res?.resourceType?.trim() && !categoryResource) {
|
|
1654
|
+
categoryResource = res;
|
|
1655
|
+
}
|
|
1656
|
+
return res?.resourceType?.trim();
|
|
1657
|
+
}).find((resourceType) => Boolean(resourceType));
|
|
1639
1658
|
const groupId = categoryType?.toLowerCase() ?? UNCATEGORIZED_CATEGORY_KEY;
|
|
1640
1659
|
const label = categoryType ? formatCategoryLabel(categoryType) : UNCATEGORIZED_CATEGORY_LABEL;
|
|
1641
1660
|
if (!groups.has(groupId)) {
|
|
1642
1661
|
groups.set(groupId, {
|
|
1643
1662
|
id: groupId,
|
|
1644
1663
|
label,
|
|
1664
|
+
imageUrl: categoryResource?.imageUrl ?? void 0,
|
|
1645
1665
|
products: []
|
|
1646
1666
|
});
|
|
1647
1667
|
}
|
|
1648
|
-
groups.get(groupId)
|
|
1668
|
+
const group = groups.get(groupId);
|
|
1669
|
+
group.products.push(product);
|
|
1670
|
+
if (!group.imageUrl && categoryResource?.imageUrl) {
|
|
1671
|
+
group.imageUrl = categoryResource.imageUrl;
|
|
1672
|
+
}
|
|
1649
1673
|
});
|
|
1650
1674
|
return Array.from(groups.values());
|
|
1651
1675
|
}
|
|
1652
|
-
function
|
|
1676
|
+
function BackArrow() {
|
|
1677
|
+
return /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1678
|
+
/* @__PURE__ */ jsx("path", { d: "M19 12H5" }),
|
|
1679
|
+
/* @__PURE__ */ jsx("path", { d: "M12 19l-7-7 7-7" })
|
|
1680
|
+
] });
|
|
1681
|
+
}
|
|
1682
|
+
function CategoryTile({
|
|
1683
|
+
group,
|
|
1684
|
+
onClick
|
|
1685
|
+
}) {
|
|
1686
|
+
return /* @__PURE__ */ jsxs(
|
|
1687
|
+
"button",
|
|
1688
|
+
{
|
|
1689
|
+
type: "button",
|
|
1690
|
+
className: "resira-category-tile",
|
|
1691
|
+
onClick,
|
|
1692
|
+
children: [
|
|
1693
|
+
/* @__PURE__ */ jsxs("div", { className: "resira-category-tile-image", children: [
|
|
1694
|
+
/* @__PURE__ */ jsx(
|
|
1695
|
+
"img",
|
|
1696
|
+
{
|
|
1697
|
+
src: group.imageUrl || PLACEHOLDER_IMG2,
|
|
1698
|
+
alt: group.label,
|
|
1699
|
+
loading: "lazy"
|
|
1700
|
+
}
|
|
1701
|
+
),
|
|
1702
|
+
/* @__PURE__ */ jsx("div", { className: "resira-category-tile-overlay" })
|
|
1703
|
+
] }),
|
|
1704
|
+
/* @__PURE__ */ jsxs("div", { className: "resira-category-tile-content", children: [
|
|
1705
|
+
/* @__PURE__ */ jsx("h3", { className: "resira-category-tile-name", children: group.label }),
|
|
1706
|
+
/* @__PURE__ */ jsxs("span", { className: "resira-category-tile-count", children: [
|
|
1707
|
+
group.products.length,
|
|
1708
|
+
" ",
|
|
1709
|
+
group.products.length === 1 ? "service" : "services"
|
|
1710
|
+
] })
|
|
1711
|
+
] })
|
|
1712
|
+
]
|
|
1713
|
+
}
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
function ServiceOverlayCard({
|
|
1653
1717
|
product,
|
|
1654
1718
|
isSelected,
|
|
1655
|
-
layout,
|
|
1656
1719
|
locale,
|
|
1657
1720
|
cardClassName
|
|
1658
1721
|
}) {
|
|
1659
1722
|
const currency = product.currency ?? "EUR";
|
|
1660
1723
|
const priceLabel = product.pricingModel === "per_rider" ? "per rider" : product.pricingModel === "per_person" ? locale.perPerson : locale.perSession;
|
|
1661
|
-
let className =
|
|
1662
|
-
if (isSelected) className += " resira-service-card--selected";
|
|
1724
|
+
let className = "resira-service-overlay-card";
|
|
1725
|
+
if (isSelected) className += " resira-service-overlay-card--selected";
|
|
1663
1726
|
if (cardClassName) className += ` ${cardClassName}`;
|
|
1664
1727
|
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
1665
|
-
/* @__PURE__ */ jsx("div", { className: "resira-service-card-
|
|
1728
|
+
/* @__PURE__ */ jsx("div", { className: "resira-service-overlay-card-bg", children: /* @__PURE__ */ jsx(
|
|
1666
1729
|
"img",
|
|
1667
1730
|
{
|
|
1668
1731
|
src: product.imageUrl ?? PLACEHOLDER_IMG2,
|
|
@@ -1670,30 +1733,27 @@ function DefaultServiceCard({
|
|
|
1670
1733
|
loading: "lazy"
|
|
1671
1734
|
}
|
|
1672
1735
|
) }),
|
|
1673
|
-
/* @__PURE__ */
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1736
|
+
/* @__PURE__ */ jsx("div", { className: "resira-service-overlay-card-gradient" }),
|
|
1737
|
+
/* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-content", children: [
|
|
1738
|
+
/* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-top", children: [
|
|
1739
|
+
/* @__PURE__ */ jsx("h3", { className: "resira-service-overlay-card-name", children: product.name }),
|
|
1740
|
+
product.description && /* @__PURE__ */ jsx("p", { className: "resira-service-overlay-card-desc", children: product.description })
|
|
1677
1741
|
] }),
|
|
1678
|
-
/* @__PURE__ */ jsxs("div", { className: "resira-service-card-bottom", children: [
|
|
1679
|
-
/* @__PURE__ */ jsxs("span", { className: "resira-service-card-price", children: [
|
|
1742
|
+
/* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-bottom", children: [
|
|
1743
|
+
/* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-price", children: [
|
|
1680
1744
|
formatPrice2(product.priceCents, currency),
|
|
1681
|
-
/* @__PURE__ */ jsxs("span", { className: "resira-service-card-price-unit", children: [
|
|
1745
|
+
/* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-price-unit", children: [
|
|
1682
1746
|
"/",
|
|
1683
1747
|
priceLabel
|
|
1684
1748
|
] })
|
|
1685
1749
|
] }),
|
|
1686
|
-
/* @__PURE__ */ jsxs("div", { className: "resira-service-card-pills", children: [
|
|
1687
|
-
product.durationMinutes > 0 && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
|
|
1688
|
-
/* @__PURE__ */ jsx(ClockIcon, { size:
|
|
1750
|
+
/* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-pills", children: [
|
|
1751
|
+
product.durationMinutes > 0 && /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-pill", children: [
|
|
1752
|
+
/* @__PURE__ */ jsx(ClockIcon, { size: 11 }),
|
|
1689
1753
|
formatDuration2(product.durationMinutes)
|
|
1690
1754
|
] }),
|
|
1691
|
-
product.
|
|
1692
|
-
/* @__PURE__ */ jsx(UsersIcon, { size:
|
|
1693
|
-
product.maxPartySize ? `1\u2013${product.maxPartySize}` : locale.perPerson
|
|
1694
|
-
] }),
|
|
1695
|
-
product.maxPartySize && product.pricingModel !== "per_person" && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
|
|
1696
|
-
/* @__PURE__ */ jsx(UsersIcon, { size: 12 }),
|
|
1755
|
+
product.maxPartySize && /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-pill", children: [
|
|
1756
|
+
/* @__PURE__ */ jsx(UsersIcon, { size: 11 }),
|
|
1697
1757
|
"max ",
|
|
1698
1758
|
product.maxPartySize
|
|
1699
1759
|
] })
|
|
@@ -1721,7 +1781,8 @@ function ProductSelector({
|
|
|
1721
1781
|
error = null,
|
|
1722
1782
|
layout: layoutProp,
|
|
1723
1783
|
visibleCount: visibleCountProp,
|
|
1724
|
-
groupByCategory: groupByCategoryProp
|
|
1784
|
+
groupByCategory: groupByCategoryProp,
|
|
1785
|
+
initialCategory
|
|
1725
1786
|
}) {
|
|
1726
1787
|
const {
|
|
1727
1788
|
locale,
|
|
@@ -1735,21 +1796,37 @@ function ProductSelector({
|
|
|
1735
1796
|
const visibleCount = visibleCountProp ?? providerCount;
|
|
1736
1797
|
const groupByCategory = groupByCategoryProp ?? providerGroupByCategory;
|
|
1737
1798
|
const containerRef = useRef(null);
|
|
1799
|
+
const [activeCategory, setActiveCategory] = useState(initialCategory ?? null);
|
|
1738
1800
|
const productGroups = useMemo(
|
|
1739
|
-
() => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", products }],
|
|
1801
|
+
() => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", imageUrl: void 0, products }],
|
|
1740
1802
|
[groupByCategory, products, resources]
|
|
1741
1803
|
);
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
|
|
1804
|
+
const skipCategoryView = productGroups.length <= 1;
|
|
1805
|
+
useEffect(() => {
|
|
1806
|
+
if (initialCategory && productGroups.some((g) => g.id === initialCategory)) {
|
|
1807
|
+
setActiveCategory(initialCategory);
|
|
1808
|
+
}
|
|
1809
|
+
}, [initialCategory, productGroups]);
|
|
1810
|
+
useEffect(() => {
|
|
1811
|
+
if (activeCategory && !productGroups.some((g) => g.id === activeCategory)) {
|
|
1812
|
+
setActiveCategory(null);
|
|
1745
1813
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1814
|
+
}, [activeCategory, productGroups]);
|
|
1815
|
+
const activeCategoryGroup = useMemo(
|
|
1816
|
+
() => productGroups.find((g) => g.id === activeCategory) ?? null,
|
|
1817
|
+
[productGroups, activeCategory]
|
|
1818
|
+
);
|
|
1819
|
+
const displayProducts = useMemo(() => {
|
|
1820
|
+
if (skipCategoryView) return products;
|
|
1821
|
+
return activeCategoryGroup?.products ?? [];
|
|
1822
|
+
}, [skipCategoryView, products, activeCategoryGroup]);
|
|
1823
|
+
const containerStyle = useMemo(() => {
|
|
1824
|
+
if (layout === "horizontal") return {};
|
|
1825
|
+
const cardHeight = 180;
|
|
1826
|
+
const gap = 12;
|
|
1827
|
+
const maxHeight = visibleCount * cardHeight + (visibleCount - 1) * gap;
|
|
1751
1828
|
return { maxHeight, overflowY: "auto" };
|
|
1752
|
-
}, [
|
|
1829
|
+
}, [layout, visibleCount]);
|
|
1753
1830
|
if (loading) {
|
|
1754
1831
|
return /* @__PURE__ */ jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
|
|
1755
1832
|
/* @__PURE__ */ jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
|
|
@@ -1762,54 +1839,64 @@ function ProductSelector({
|
|
|
1762
1839
|
if (products.length === 0) {
|
|
1763
1840
|
return /* @__PURE__ */ jsx("div", { className: "resira-empty", children: /* @__PURE__ */ jsx("p", { children: "No services available at the moment." }) });
|
|
1764
1841
|
}
|
|
1842
|
+
if (groupByCategory && !skipCategoryView && !activeCategory) {
|
|
1843
|
+
return /* @__PURE__ */ jsx("div", { className: "resira-service-picker", children: /* @__PURE__ */ jsx("div", { className: "resira-category-grid", children: productGroups.map((group) => /* @__PURE__ */ jsx(
|
|
1844
|
+
CategoryTile,
|
|
1845
|
+
{
|
|
1846
|
+
group,
|
|
1847
|
+
onClick: () => setActiveCategory(group.id)
|
|
1848
|
+
},
|
|
1849
|
+
group.id
|
|
1850
|
+
)) }) });
|
|
1851
|
+
}
|
|
1765
1852
|
const listClassName = [
|
|
1766
|
-
"resira-service-list",
|
|
1767
|
-
`resira-service-list--${layout}`,
|
|
1853
|
+
"resira-service-overlay-list",
|
|
1768
1854
|
classNames.serviceList
|
|
1769
1855
|
].filter(Boolean).join(" ");
|
|
1770
|
-
const groupedListClassName = [
|
|
1771
|
-
listClassName,
|
|
1772
|
-
groupByCategory ? "resira-service-list--grouped" : ""
|
|
1773
|
-
].filter(Boolean).join(" ");
|
|
1774
1856
|
return /* @__PURE__ */ jsxs("div", { className: "resira-service-picker", children: [
|
|
1857
|
+
groupByCategory && !skipCategoryView && activeCategory && /* @__PURE__ */ jsxs(
|
|
1858
|
+
"button",
|
|
1859
|
+
{
|
|
1860
|
+
type: "button",
|
|
1861
|
+
className: "resira-category-back",
|
|
1862
|
+
onClick: () => setActiveCategory(null),
|
|
1863
|
+
children: [
|
|
1864
|
+
/* @__PURE__ */ jsx(BackArrow, {}),
|
|
1865
|
+
/* @__PURE__ */ jsx("span", { children: activeCategoryGroup?.label ?? "All categories" })
|
|
1866
|
+
]
|
|
1867
|
+
}
|
|
1868
|
+
),
|
|
1775
1869
|
/* @__PURE__ */ jsx(
|
|
1776
1870
|
"div",
|
|
1777
1871
|
{
|
|
1778
1872
|
ref: containerRef,
|
|
1779
|
-
className:
|
|
1873
|
+
className: listClassName,
|
|
1780
1874
|
style: containerStyle,
|
|
1781
|
-
children:
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
"
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
}
|
|
1804
|
-
)
|
|
1805
|
-
},
|
|
1806
|
-
product.id
|
|
1807
|
-
);
|
|
1808
|
-
}) })
|
|
1809
|
-
] }, group.id))
|
|
1875
|
+
children: displayProducts.map((product) => {
|
|
1876
|
+
const isSelected = selectedId === product.id;
|
|
1877
|
+
return /* @__PURE__ */ jsx(
|
|
1878
|
+
"button",
|
|
1879
|
+
{
|
|
1880
|
+
type: "button",
|
|
1881
|
+
className: "resira-service-card-btn",
|
|
1882
|
+
onClick: () => onSelect(product),
|
|
1883
|
+
"aria-pressed": isSelected,
|
|
1884
|
+
children: renderServiceCard ? renderServiceCard(product, isSelected) : /* @__PURE__ */ jsx(
|
|
1885
|
+
ServiceOverlayCard,
|
|
1886
|
+
{
|
|
1887
|
+
product,
|
|
1888
|
+
isSelected,
|
|
1889
|
+
locale,
|
|
1890
|
+
cardClassName: isSelected ? classNames.serviceCardSelected : classNames.serviceCard
|
|
1891
|
+
}
|
|
1892
|
+
)
|
|
1893
|
+
},
|
|
1894
|
+
product.id
|
|
1895
|
+
);
|
|
1896
|
+
})
|
|
1810
1897
|
}
|
|
1811
1898
|
),
|
|
1812
|
-
|
|
1899
|
+
displayProducts.length > visibleCount && /* @__PURE__ */ jsxs("div", { className: "resira-service-scroll-hint", children: [
|
|
1813
1900
|
/* @__PURE__ */ jsx("span", { children: "Scroll for more" }),
|
|
1814
1901
|
/* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4" }) })
|
|
1815
1902
|
] })
|
|
@@ -2604,7 +2691,13 @@ function ResiraBookingWidget() {
|
|
|
2604
2691
|
showWaiver,
|
|
2605
2692
|
showRemainingSpots,
|
|
2606
2693
|
depositPercent,
|
|
2607
|
-
onClose
|
|
2694
|
+
onClose,
|
|
2695
|
+
showStepIndicator,
|
|
2696
|
+
deeplink,
|
|
2697
|
+
deeplinkGuest,
|
|
2698
|
+
onStepChange,
|
|
2699
|
+
onBookingComplete,
|
|
2700
|
+
onError
|
|
2608
2701
|
} = useResira();
|
|
2609
2702
|
const isDateBased = domain === "rental";
|
|
2610
2703
|
const isTimeBased = domain === "restaurant" || domain === "watersport" || domain === "service";
|
|
@@ -2962,8 +3055,9 @@ function ResiraBookingWidget() {
|
|
|
2962
3055
|
setStep("confirmation");
|
|
2963
3056
|
}
|
|
2964
3057
|
}, [createPayment, paymentPayload, confirmPayment]);
|
|
2965
|
-
const handlePaymentError = useCallback((
|
|
2966
|
-
|
|
3058
|
+
const handlePaymentError = useCallback((msg) => {
|
|
3059
|
+
onError?.("payment_error", msg);
|
|
3060
|
+
}, [onError]);
|
|
2967
3061
|
const handleSubmitNoPayment = useCallback(async () => {
|
|
2968
3062
|
if (showTerms && !termsAccepted) {
|
|
2969
3063
|
setTermsError(locale.termsRequired);
|
|
@@ -2993,9 +3087,63 @@ function ResiraBookingWidget() {
|
|
|
2993
3087
|
}, [guest, selection, locale, submit, termsAccepted, waiverAccepted, showTerms, showWaiver, activeResourceId, selectedProduct]);
|
|
2994
3088
|
const footerBusy = submitting || creatingPayment || confirmingPayment || paymentFormSubmitting;
|
|
2995
3089
|
const paymentActionDisabled = !paymentIntent || !paymentFormReady || paymentFormSubmitting || confirmingPayment;
|
|
3090
|
+
const [deeplinkApplied, setDeeplinkApplied] = useState(false);
|
|
3091
|
+
useEffect(() => {
|
|
3092
|
+
if (deeplinkApplied || !deeplink) return;
|
|
3093
|
+
if (deeplink.productId && products.length > 0 && !selectedProduct) {
|
|
3094
|
+
const target = products.find((p) => p.id === deeplink.productId);
|
|
3095
|
+
if (target) {
|
|
3096
|
+
handleProductSelect(target);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
if (deeplink.partySize || deeplink.duration || deeplink.date) {
|
|
3100
|
+
setSelection((prev) => ({
|
|
3101
|
+
...prev,
|
|
3102
|
+
...deeplink.partySize ? { partySize: deeplink.partySize } : {},
|
|
3103
|
+
...deeplink.duration ? { duration: deeplink.duration } : {},
|
|
3104
|
+
...deeplink.date ? { startDate: deeplink.date } : {}
|
|
3105
|
+
}));
|
|
3106
|
+
if (deeplink.date) setSlotDate(deeplink.date);
|
|
3107
|
+
}
|
|
3108
|
+
if (deeplinkGuest) {
|
|
3109
|
+
setGuest((prev) => ({
|
|
3110
|
+
guestName: deeplinkGuest.name ?? prev.guestName,
|
|
3111
|
+
guestEmail: deeplinkGuest.email ?? prev.guestEmail,
|
|
3112
|
+
guestPhone: deeplinkGuest.phone ?? prev.guestPhone,
|
|
3113
|
+
notes: deeplinkGuest.notes ?? prev.notes
|
|
3114
|
+
}));
|
|
3115
|
+
}
|
|
3116
|
+
if (deeplink.productId && step === "resource" && isServiceBased) {
|
|
3117
|
+
const target = products.find((p) => p.id === deeplink.productId);
|
|
3118
|
+
if (target) {
|
|
3119
|
+
setStep(STEPS[stepIndex("resource", STEPS) + 1]);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
setDeeplinkApplied(true);
|
|
3123
|
+
}, [deeplink, deeplinkGuest, products, selectedProduct, step, isServiceBased, STEPS, handleProductSelect]);
|
|
3124
|
+
useEffect(() => {
|
|
3125
|
+
onStepChange?.(step);
|
|
3126
|
+
}, [step, onStepChange]);
|
|
3127
|
+
useEffect(() => {
|
|
3128
|
+
if (step === "confirmation") {
|
|
3129
|
+
const bookingId = reservation?.id ?? paymentIntent?.reservationId;
|
|
3130
|
+
onBookingComplete?.({
|
|
3131
|
+
reservationId: bookingId,
|
|
3132
|
+
product: selectedProduct ?? void 0,
|
|
3133
|
+
selection,
|
|
3134
|
+
guest
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
}, [step]);
|
|
3138
|
+
useEffect(() => {
|
|
3139
|
+
if (submitError) onError?.("submit_error", submitError);
|
|
3140
|
+
}, [submitError, onError]);
|
|
3141
|
+
useEffect(() => {
|
|
3142
|
+
if (paymentError) onError?.("payment_error", paymentError);
|
|
3143
|
+
}, [paymentError, onError]);
|
|
2996
3144
|
return /* @__PURE__ */ jsxs("div", { className: "resira-widget", children: [
|
|
2997
3145
|
step !== "confirmation" && /* @__PURE__ */ jsxs("div", { className: "resira-widget-topbar", children: [
|
|
2998
|
-
/* @__PURE__ */ jsx("nav", { className: "resira-steps", "aria-label": "Booking steps", children: STEPS.filter((s) => s !== "confirmation").map((s, i) => {
|
|
3146
|
+
showStepIndicator && /* @__PURE__ */ jsx("nav", { className: "resira-steps", "aria-label": "Booking steps", children: STEPS.filter((s) => s !== "confirmation").map((s, i) => {
|
|
2999
3147
|
const isCompleted = stepIndex(step, STEPS) > i;
|
|
3000
3148
|
const isActive = s === step;
|
|
3001
3149
|
return /* @__PURE__ */ jsxs(
|