@resira/ui 0.3.1 → 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/README.md +5 -3
- package/dist/index.cjs +264 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -6
- package/dist/index.d.ts +61 -6
- package/dist/index.js +264 -103
- package/dist/index.js.map +1 -1
- package/dist/styles.css +232 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# @resira/ui v0.3.
|
|
1
|
+
# @resira/ui v0.3.2
|
|
2
2
|
|
|
3
3
|
React booking UI for Resira. It includes a ready-to-embed widget, modal flow, provider, hooks, and lower-level components for custom booking experiences.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @resira/ui@0.3.
|
|
8
|
+
npm install @resira/ui@0.3.2 @resira/sdk@0.3.1
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Peer dependencies:
|
|
@@ -103,7 +103,7 @@ import "@resira/ui/styles.css";
|
|
|
103
103
|
| `groupServicesByCategory` | `boolean` | Group services by linked equipment type |
|
|
104
104
|
| `renderServiceCard` | `(product, selected) => ReactNode` | Custom service card rendering |
|
|
105
105
|
| `baseUrl` | `string` | Optional API origin override |
|
|
106
|
-
| `baseUrls` | `Array<string \| WeightedBaseUrl>` | Optional
|
|
106
|
+
| `baseUrls` | `Array<string \| WeightedBaseUrl>` | Optional ordered fallback origin list |
|
|
107
107
|
| `stripePublishableKey` | `string` | Stripe publishable key override |
|
|
108
108
|
| `termsText` | `string` | Terms text override |
|
|
109
109
|
| `waiverText` | `string` | Waiver text override |
|
|
@@ -233,4 +233,6 @@ import { DishShowcase } from "@resira/ui";
|
|
|
233
233
|
|
|
234
234
|
- The widget supports catalog mode when `resourceId` is omitted.
|
|
235
235
|
- Service and watersport flows group services by linked equipment category by default.
|
|
236
|
+
- Loading and error states now announce themselves to assistive technologies on the main widget surfaces.
|
|
237
|
+
- Client-side API origin stickiness/load-balancing has been removed; routing is deterministic unless you pass explicit fallback origins.
|
|
236
238
|
- The package is intended for browser usage and ships CSS separately via `@resira/ui/styles.css`.
|
package/dist/index.cjs
CHANGED
|
@@ -224,6 +224,12 @@ function ResiraProvider({
|
|
|
224
224
|
const visibleServiceCount = config?.visibleServiceCount ?? 4;
|
|
225
225
|
const groupServicesByCategory = config?.groupServicesByCategory ?? true;
|
|
226
226
|
const renderServiceCard = config?.renderServiceCard;
|
|
227
|
+
const showStepIndicator = config?.showStepIndicator ?? true;
|
|
228
|
+
const deeplink = config?.deeplink;
|
|
229
|
+
const deeplinkGuest = config?.deeplinkGuest;
|
|
230
|
+
const onStepChange = config?.onStepChange;
|
|
231
|
+
const onBookingComplete = config?.onBookingComplete;
|
|
232
|
+
const onError = config?.onError;
|
|
227
233
|
const cssVars = react.useMemo(() => themeToCSS(theme), [theme]);
|
|
228
234
|
const value = react.useMemo(
|
|
229
235
|
() => ({
|
|
@@ -250,9 +256,15 @@ function ResiraProvider({
|
|
|
250
256
|
serviceLayout,
|
|
251
257
|
visibleServiceCount,
|
|
252
258
|
groupServicesByCategory,
|
|
253
|
-
renderServiceCard
|
|
259
|
+
renderServiceCard,
|
|
260
|
+
showStepIndicator,
|
|
261
|
+
deeplink,
|
|
262
|
+
deeplinkGuest,
|
|
263
|
+
onStepChange,
|
|
264
|
+
onBookingComplete,
|
|
265
|
+
onError
|
|
254
266
|
}),
|
|
255
|
-
[client, resourceId, activeResourceId, setActiveResourceId, catalogMode, allowMultiSelect, domain, theme, locale, domainConfig, stripePublishableKey, termsText, waiverText, showWaiver, showTerms, showRemainingSpots, depositPercent, refundPolicy, onClose, classNames, serviceLayout, visibleServiceCount, groupServicesByCategory, renderServiceCard]
|
|
267
|
+
[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]
|
|
256
268
|
);
|
|
257
269
|
return /* @__PURE__ */ jsxRuntime.jsx(ResiraContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-root", style: cssVars, children }) });
|
|
258
270
|
}
|
|
@@ -547,12 +559,17 @@ function useDish(dishId) {
|
|
|
547
559
|
}, [client, dishId]);
|
|
548
560
|
return { dish, loading, error };
|
|
549
561
|
}
|
|
550
|
-
function useDishes() {
|
|
562
|
+
function useDishes(enabled = true) {
|
|
551
563
|
const { client } = useResira();
|
|
552
564
|
const [dishes, setDishes] = react.useState([]);
|
|
553
|
-
const [loading, setLoading] = react.useState(
|
|
565
|
+
const [loading, setLoading] = react.useState(enabled);
|
|
554
566
|
const [error, setError] = react.useState(null);
|
|
555
567
|
react.useEffect(() => {
|
|
568
|
+
if (!enabled) {
|
|
569
|
+
setLoading(false);
|
|
570
|
+
setError(null);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
556
573
|
let cancelled = false;
|
|
557
574
|
async function fetchDishes() {
|
|
558
575
|
try {
|
|
@@ -576,7 +593,7 @@ function useDishes() {
|
|
|
576
593
|
return () => {
|
|
577
594
|
cancelled = true;
|
|
578
595
|
};
|
|
579
|
-
}, [client]);
|
|
596
|
+
}, [client, enabled]);
|
|
580
597
|
return { dishes, loading, error };
|
|
581
598
|
}
|
|
582
599
|
var defaultSize = 20;
|
|
@@ -1542,13 +1559,13 @@ function ResourcePicker({
|
|
|
1542
1559
|
error = null
|
|
1543
1560
|
}) {
|
|
1544
1561
|
if (loading) {
|
|
1545
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", children: [
|
|
1546
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner" }),
|
|
1562
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
|
|
1563
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
|
|
1547
1564
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-loading-text", children: "Loading resources\u2026" })
|
|
1548
1565
|
] });
|
|
1549
1566
|
}
|
|
1550
1567
|
if (error) {
|
|
1551
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-error", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-error-message", children: error }) });
|
|
1568
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-error", role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-error-message", children: error }) });
|
|
1552
1569
|
}
|
|
1553
1570
|
if (resources.length === 0) {
|
|
1554
1571
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-empty", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No resources available at the moment." }) });
|
|
@@ -1627,39 +1644,90 @@ function formatCategoryLabel(resourceType) {
|
|
|
1627
1644
|
return resourceType.trim().split(/[_\-\s]+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()).join(" ");
|
|
1628
1645
|
}
|
|
1629
1646
|
function groupProductsByCategory(products, resources) {
|
|
1630
|
-
const
|
|
1631
|
-
resources.map((resource) => [resource.id, resource
|
|
1647
|
+
const resourceById = new Map(
|
|
1648
|
+
resources.map((resource) => [resource.id, resource])
|
|
1632
1649
|
);
|
|
1633
1650
|
const groups = /* @__PURE__ */ new Map();
|
|
1634
1651
|
products.forEach((product) => {
|
|
1635
|
-
|
|
1652
|
+
let categoryResource;
|
|
1653
|
+
const categoryType = product.equipmentIds.map((equipmentId) => {
|
|
1654
|
+
const res = resourceById.get(equipmentId);
|
|
1655
|
+
if (res?.resourceType?.trim() && !categoryResource) {
|
|
1656
|
+
categoryResource = res;
|
|
1657
|
+
}
|
|
1658
|
+
return res?.resourceType?.trim();
|
|
1659
|
+
}).find((resourceType) => Boolean(resourceType));
|
|
1636
1660
|
const groupId = categoryType?.toLowerCase() ?? UNCATEGORIZED_CATEGORY_KEY;
|
|
1637
1661
|
const label = categoryType ? formatCategoryLabel(categoryType) : UNCATEGORIZED_CATEGORY_LABEL;
|
|
1638
1662
|
if (!groups.has(groupId)) {
|
|
1639
1663
|
groups.set(groupId, {
|
|
1640
1664
|
id: groupId,
|
|
1641
1665
|
label,
|
|
1666
|
+
imageUrl: categoryResource?.imageUrl ?? void 0,
|
|
1642
1667
|
products: []
|
|
1643
1668
|
});
|
|
1644
1669
|
}
|
|
1645
|
-
groups.get(groupId)
|
|
1670
|
+
const group = groups.get(groupId);
|
|
1671
|
+
group.products.push(product);
|
|
1672
|
+
if (!group.imageUrl && categoryResource?.imageUrl) {
|
|
1673
|
+
group.imageUrl = categoryResource.imageUrl;
|
|
1674
|
+
}
|
|
1646
1675
|
});
|
|
1647
1676
|
return Array.from(groups.values());
|
|
1648
1677
|
}
|
|
1649
|
-
function
|
|
1678
|
+
function BackArrow() {
|
|
1679
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1680
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 12H5" }),
|
|
1681
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19l-7-7 7-7" })
|
|
1682
|
+
] });
|
|
1683
|
+
}
|
|
1684
|
+
function CategoryTile({
|
|
1685
|
+
group,
|
|
1686
|
+
onClick
|
|
1687
|
+
}) {
|
|
1688
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1689
|
+
"button",
|
|
1690
|
+
{
|
|
1691
|
+
type: "button",
|
|
1692
|
+
className: "resira-category-tile",
|
|
1693
|
+
onClick,
|
|
1694
|
+
children: [
|
|
1695
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-category-tile-image", children: [
|
|
1696
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1697
|
+
"img",
|
|
1698
|
+
{
|
|
1699
|
+
src: group.imageUrl || PLACEHOLDER_IMG2,
|
|
1700
|
+
alt: group.label,
|
|
1701
|
+
loading: "lazy"
|
|
1702
|
+
}
|
|
1703
|
+
),
|
|
1704
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-category-tile-overlay" })
|
|
1705
|
+
] }),
|
|
1706
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-category-tile-content", children: [
|
|
1707
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "resira-category-tile-name", children: group.label }),
|
|
1708
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-category-tile-count", children: [
|
|
1709
|
+
group.products.length,
|
|
1710
|
+
" ",
|
|
1711
|
+
group.products.length === 1 ? "service" : "services"
|
|
1712
|
+
] })
|
|
1713
|
+
] })
|
|
1714
|
+
]
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
function ServiceOverlayCard({
|
|
1650
1719
|
product,
|
|
1651
1720
|
isSelected,
|
|
1652
|
-
layout,
|
|
1653
1721
|
locale,
|
|
1654
1722
|
cardClassName
|
|
1655
1723
|
}) {
|
|
1656
1724
|
const currency = product.currency ?? "EUR";
|
|
1657
1725
|
const priceLabel = product.pricingModel === "per_rider" ? "per rider" : product.pricingModel === "per_person" ? locale.perPerson : locale.perSession;
|
|
1658
|
-
let className =
|
|
1659
|
-
if (isSelected) className += " resira-service-card--selected";
|
|
1726
|
+
let className = "resira-service-overlay-card";
|
|
1727
|
+
if (isSelected) className += " resira-service-overlay-card--selected";
|
|
1660
1728
|
if (cardClassName) className += ` ${cardClassName}`;
|
|
1661
1729
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
|
|
1662
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-service-card-
|
|
1730
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-service-overlay-card-bg", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1663
1731
|
"img",
|
|
1664
1732
|
{
|
|
1665
1733
|
src: product.imageUrl ?? PLACEHOLDER_IMG2,
|
|
@@ -1667,30 +1735,27 @@ function DefaultServiceCard({
|
|
|
1667
1735
|
loading: "lazy"
|
|
1668
1736
|
}
|
|
1669
1737
|
) }),
|
|
1670
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1738
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-service-overlay-card-gradient" }),
|
|
1739
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-overlay-card-content", children: [
|
|
1740
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-overlay-card-top", children: [
|
|
1741
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "resira-service-overlay-card-name", children: product.name }),
|
|
1742
|
+
product.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-service-overlay-card-desc", children: product.description })
|
|
1674
1743
|
] }),
|
|
1675
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-card-bottom", children: [
|
|
1676
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-card-price", children: [
|
|
1744
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-overlay-card-bottom", children: [
|
|
1745
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-overlay-card-price", children: [
|
|
1677
1746
|
formatPrice2(product.priceCents, currency),
|
|
1678
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-card-price-unit", children: [
|
|
1747
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-overlay-card-price-unit", children: [
|
|
1679
1748
|
"/",
|
|
1680
1749
|
priceLabel
|
|
1681
1750
|
] })
|
|
1682
1751
|
] }),
|
|
1683
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-card-pills", children: [
|
|
1684
|
-
product.durationMinutes > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-card-pill", children: [
|
|
1685
|
-
/* @__PURE__ */ jsxRuntime.jsx(ClockIcon, { size:
|
|
1752
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-overlay-card-pills", children: [
|
|
1753
|
+
product.durationMinutes > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-overlay-card-pill", children: [
|
|
1754
|
+
/* @__PURE__ */ jsxRuntime.jsx(ClockIcon, { size: 11 }),
|
|
1686
1755
|
formatDuration2(product.durationMinutes)
|
|
1687
1756
|
] }),
|
|
1688
|
-
product.
|
|
1689
|
-
/* @__PURE__ */ jsxRuntime.jsx(UsersIcon, { size:
|
|
1690
|
-
product.maxPartySize ? `1\u2013${product.maxPartySize}` : locale.perPerson
|
|
1691
|
-
] }),
|
|
1692
|
-
product.maxPartySize && product.pricingModel !== "per_person" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-card-pill", children: [
|
|
1693
|
-
/* @__PURE__ */ jsxRuntime.jsx(UsersIcon, { size: 12 }),
|
|
1757
|
+
product.maxPartySize && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-service-overlay-card-pill", children: [
|
|
1758
|
+
/* @__PURE__ */ jsxRuntime.jsx(UsersIcon, { size: 11 }),
|
|
1694
1759
|
"max ",
|
|
1695
1760
|
product.maxPartySize
|
|
1696
1761
|
] })
|
|
@@ -1718,7 +1783,8 @@ function ProductSelector({
|
|
|
1718
1783
|
error = null,
|
|
1719
1784
|
layout: layoutProp,
|
|
1720
1785
|
visibleCount: visibleCountProp,
|
|
1721
|
-
groupByCategory: groupByCategoryProp
|
|
1786
|
+
groupByCategory: groupByCategoryProp,
|
|
1787
|
+
initialCategory
|
|
1722
1788
|
}) {
|
|
1723
1789
|
const {
|
|
1724
1790
|
locale,
|
|
@@ -1732,81 +1798,107 @@ function ProductSelector({
|
|
|
1732
1798
|
const visibleCount = visibleCountProp ?? providerCount;
|
|
1733
1799
|
const groupByCategory = groupByCategoryProp ?? providerGroupByCategory;
|
|
1734
1800
|
const containerRef = react.useRef(null);
|
|
1801
|
+
const [activeCategory, setActiveCategory] = react.useState(initialCategory ?? null);
|
|
1735
1802
|
const productGroups = react.useMemo(
|
|
1736
|
-
() => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", products }],
|
|
1803
|
+
() => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", imageUrl: void 0, products }],
|
|
1737
1804
|
[groupByCategory, products, resources]
|
|
1738
1805
|
);
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1806
|
+
const skipCategoryView = productGroups.length <= 1;
|
|
1807
|
+
react.useEffect(() => {
|
|
1808
|
+
if (initialCategory && productGroups.some((g) => g.id === initialCategory)) {
|
|
1809
|
+
setActiveCategory(initialCategory);
|
|
1810
|
+
}
|
|
1811
|
+
}, [initialCategory, productGroups]);
|
|
1812
|
+
react.useEffect(() => {
|
|
1813
|
+
if (activeCategory && !productGroups.some((g) => g.id === activeCategory)) {
|
|
1814
|
+
setActiveCategory(null);
|
|
1742
1815
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1816
|
+
}, [activeCategory, productGroups]);
|
|
1817
|
+
const activeCategoryGroup = react.useMemo(
|
|
1818
|
+
() => productGroups.find((g) => g.id === activeCategory) ?? null,
|
|
1819
|
+
[productGroups, activeCategory]
|
|
1820
|
+
);
|
|
1821
|
+
const displayProducts = react.useMemo(() => {
|
|
1822
|
+
if (skipCategoryView) return products;
|
|
1823
|
+
return activeCategoryGroup?.products ?? [];
|
|
1824
|
+
}, [skipCategoryView, products, activeCategoryGroup]);
|
|
1825
|
+
const containerStyle = react.useMemo(() => {
|
|
1826
|
+
if (layout === "horizontal") return {};
|
|
1827
|
+
const cardHeight = 180;
|
|
1828
|
+
const gap = 12;
|
|
1829
|
+
const maxHeight = visibleCount * cardHeight + (visibleCount - 1) * gap;
|
|
1748
1830
|
return { maxHeight, overflowY: "auto" };
|
|
1749
|
-
}, [
|
|
1831
|
+
}, [layout, visibleCount]);
|
|
1750
1832
|
if (loading) {
|
|
1751
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", children: [
|
|
1752
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner" }),
|
|
1833
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
|
|
1834
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
|
|
1753
1835
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-loading-text", children: locale.loading })
|
|
1754
1836
|
] });
|
|
1755
1837
|
}
|
|
1756
1838
|
if (error) {
|
|
1757
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-error", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-error-message", children: error }) });
|
|
1839
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-error", role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-error-message", children: error }) });
|
|
1758
1840
|
}
|
|
1759
1841
|
if (products.length === 0) {
|
|
1760
1842
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-empty", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No services available at the moment." }) });
|
|
1761
1843
|
}
|
|
1844
|
+
if (groupByCategory && !skipCategoryView && !activeCategory) {
|
|
1845
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-service-picker", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-category-grid", children: productGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1846
|
+
CategoryTile,
|
|
1847
|
+
{
|
|
1848
|
+
group,
|
|
1849
|
+
onClick: () => setActiveCategory(group.id)
|
|
1850
|
+
},
|
|
1851
|
+
group.id
|
|
1852
|
+
)) }) });
|
|
1853
|
+
}
|
|
1762
1854
|
const listClassName = [
|
|
1763
|
-
"resira-service-list",
|
|
1764
|
-
`resira-service-list--${layout}`,
|
|
1855
|
+
"resira-service-overlay-list",
|
|
1765
1856
|
classNames.serviceList
|
|
1766
1857
|
].filter(Boolean).join(" ");
|
|
1767
|
-
const groupedListClassName = [
|
|
1768
|
-
listClassName,
|
|
1769
|
-
groupByCategory ? "resira-service-list--grouped" : ""
|
|
1770
|
-
].filter(Boolean).join(" ");
|
|
1771
1858
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-picker", children: [
|
|
1859
|
+
groupByCategory && !skipCategoryView && activeCategory && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1860
|
+
"button",
|
|
1861
|
+
{
|
|
1862
|
+
type: "button",
|
|
1863
|
+
className: "resira-category-back",
|
|
1864
|
+
onClick: () => setActiveCategory(null),
|
|
1865
|
+
children: [
|
|
1866
|
+
/* @__PURE__ */ jsxRuntime.jsx(BackArrow, {}),
|
|
1867
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: activeCategoryGroup?.label ?? "All categories" })
|
|
1868
|
+
]
|
|
1869
|
+
}
|
|
1870
|
+
),
|
|
1772
1871
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1773
1872
|
"div",
|
|
1774
1873
|
{
|
|
1775
1874
|
ref: containerRef,
|
|
1776
|
-
className:
|
|
1875
|
+
className: listClassName,
|
|
1777
1876
|
style: containerStyle,
|
|
1778
|
-
children:
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
"
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
}
|
|
1801
|
-
)
|
|
1802
|
-
},
|
|
1803
|
-
product.id
|
|
1804
|
-
);
|
|
1805
|
-
}) })
|
|
1806
|
-
] }, group.id))
|
|
1877
|
+
children: displayProducts.map((product) => {
|
|
1878
|
+
const isSelected = selectedId === product.id;
|
|
1879
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1880
|
+
"button",
|
|
1881
|
+
{
|
|
1882
|
+
type: "button",
|
|
1883
|
+
className: "resira-service-card-btn",
|
|
1884
|
+
onClick: () => onSelect(product),
|
|
1885
|
+
"aria-pressed": isSelected,
|
|
1886
|
+
children: renderServiceCard ? renderServiceCard(product, isSelected) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
1887
|
+
ServiceOverlayCard,
|
|
1888
|
+
{
|
|
1889
|
+
product,
|
|
1890
|
+
isSelected,
|
|
1891
|
+
locale,
|
|
1892
|
+
cardClassName: isSelected ? classNames.serviceCardSelected : classNames.serviceCard
|
|
1893
|
+
}
|
|
1894
|
+
)
|
|
1895
|
+
},
|
|
1896
|
+
product.id
|
|
1897
|
+
);
|
|
1898
|
+
})
|
|
1807
1899
|
}
|
|
1808
1900
|
),
|
|
1809
|
-
|
|
1901
|
+
displayProducts.length > visibleCount && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-service-scroll-hint", children: [
|
|
1810
1902
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Scroll for more" }),
|
|
1811
1903
|
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 6l4 4 4-4" }) })
|
|
1812
1904
|
] })
|
|
@@ -2330,8 +2422,8 @@ function PaymentForm({
|
|
|
2330
2422
|
] })
|
|
2331
2423
|
] }) }),
|
|
2332
2424
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-payment-element-container", children: [
|
|
2333
|
-
!ready && !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", style: { padding: "24px 0" }, children: [
|
|
2334
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner" }),
|
|
2425
|
+
!ready && !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", style: { padding: "24px 0" }, role: "status", "aria-live": "polite", "aria-busy": "true", children: [
|
|
2426
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
|
|
2335
2427
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-loading-text", children: locale.loading })
|
|
2336
2428
|
] }),
|
|
2337
2429
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2343,7 +2435,7 @@ function PaymentForm({
|
|
|
2343
2435
|
}
|
|
2344
2436
|
)
|
|
2345
2437
|
] }),
|
|
2346
|
-
error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-payment-error", children: [
|
|
2438
|
+
error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-payment-error", role: "alert", "aria-live": "assertive", children: [
|
|
2347
2439
|
/* @__PURE__ */ jsxRuntime.jsx(AlertCircleIcon, { size: 16 }),
|
|
2348
2440
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
|
|
2349
2441
|
] }),
|
|
@@ -2601,7 +2693,13 @@ function ResiraBookingWidget() {
|
|
|
2601
2693
|
showWaiver,
|
|
2602
2694
|
showRemainingSpots,
|
|
2603
2695
|
depositPercent,
|
|
2604
|
-
onClose
|
|
2696
|
+
onClose,
|
|
2697
|
+
showStepIndicator,
|
|
2698
|
+
deeplink,
|
|
2699
|
+
deeplinkGuest,
|
|
2700
|
+
onStepChange,
|
|
2701
|
+
onBookingComplete,
|
|
2702
|
+
onError
|
|
2605
2703
|
} = useResira();
|
|
2606
2704
|
const isDateBased = domain === "rental";
|
|
2607
2705
|
const isTimeBased = domain === "restaurant" || domain === "watersport" || domain === "service";
|
|
@@ -2959,8 +3057,9 @@ function ResiraBookingWidget() {
|
|
|
2959
3057
|
setStep("confirmation");
|
|
2960
3058
|
}
|
|
2961
3059
|
}, [createPayment, paymentPayload, confirmPayment]);
|
|
2962
|
-
const handlePaymentError = react.useCallback((
|
|
2963
|
-
|
|
3060
|
+
const handlePaymentError = react.useCallback((msg) => {
|
|
3061
|
+
onError?.("payment_error", msg);
|
|
3062
|
+
}, [onError]);
|
|
2964
3063
|
const handleSubmitNoPayment = react.useCallback(async () => {
|
|
2965
3064
|
if (showTerms && !termsAccepted) {
|
|
2966
3065
|
setTermsError(locale.termsRequired);
|
|
@@ -2990,9 +3089,63 @@ function ResiraBookingWidget() {
|
|
|
2990
3089
|
}, [guest, selection, locale, submit, termsAccepted, waiverAccepted, showTerms, showWaiver, activeResourceId, selectedProduct]);
|
|
2991
3090
|
const footerBusy = submitting || creatingPayment || confirmingPayment || paymentFormSubmitting;
|
|
2992
3091
|
const paymentActionDisabled = !paymentIntent || !paymentFormReady || paymentFormSubmitting || confirmingPayment;
|
|
3092
|
+
const [deeplinkApplied, setDeeplinkApplied] = react.useState(false);
|
|
3093
|
+
react.useEffect(() => {
|
|
3094
|
+
if (deeplinkApplied || !deeplink) return;
|
|
3095
|
+
if (deeplink.productId && products.length > 0 && !selectedProduct) {
|
|
3096
|
+
const target = products.find((p) => p.id === deeplink.productId);
|
|
3097
|
+
if (target) {
|
|
3098
|
+
handleProductSelect(target);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
if (deeplink.partySize || deeplink.duration || deeplink.date) {
|
|
3102
|
+
setSelection((prev) => ({
|
|
3103
|
+
...prev,
|
|
3104
|
+
...deeplink.partySize ? { partySize: deeplink.partySize } : {},
|
|
3105
|
+
...deeplink.duration ? { duration: deeplink.duration } : {},
|
|
3106
|
+
...deeplink.date ? { startDate: deeplink.date } : {}
|
|
3107
|
+
}));
|
|
3108
|
+
if (deeplink.date) setSlotDate(deeplink.date);
|
|
3109
|
+
}
|
|
3110
|
+
if (deeplinkGuest) {
|
|
3111
|
+
setGuest((prev) => ({
|
|
3112
|
+
guestName: deeplinkGuest.name ?? prev.guestName,
|
|
3113
|
+
guestEmail: deeplinkGuest.email ?? prev.guestEmail,
|
|
3114
|
+
guestPhone: deeplinkGuest.phone ?? prev.guestPhone,
|
|
3115
|
+
notes: deeplinkGuest.notes ?? prev.notes
|
|
3116
|
+
}));
|
|
3117
|
+
}
|
|
3118
|
+
if (deeplink.productId && step === "resource" && isServiceBased) {
|
|
3119
|
+
const target = products.find((p) => p.id === deeplink.productId);
|
|
3120
|
+
if (target) {
|
|
3121
|
+
setStep(STEPS[stepIndex("resource", STEPS) + 1]);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
setDeeplinkApplied(true);
|
|
3125
|
+
}, [deeplink, deeplinkGuest, products, selectedProduct, step, isServiceBased, STEPS, handleProductSelect]);
|
|
3126
|
+
react.useEffect(() => {
|
|
3127
|
+
onStepChange?.(step);
|
|
3128
|
+
}, [step, onStepChange]);
|
|
3129
|
+
react.useEffect(() => {
|
|
3130
|
+
if (step === "confirmation") {
|
|
3131
|
+
const bookingId = reservation?.id ?? paymentIntent?.reservationId;
|
|
3132
|
+
onBookingComplete?.({
|
|
3133
|
+
reservationId: bookingId,
|
|
3134
|
+
product: selectedProduct ?? void 0,
|
|
3135
|
+
selection,
|
|
3136
|
+
guest
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
}, [step]);
|
|
3140
|
+
react.useEffect(() => {
|
|
3141
|
+
if (submitError) onError?.("submit_error", submitError);
|
|
3142
|
+
}, [submitError, onError]);
|
|
3143
|
+
react.useEffect(() => {
|
|
3144
|
+
if (paymentError) onError?.("payment_error", paymentError);
|
|
3145
|
+
}, [paymentError, onError]);
|
|
2993
3146
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-widget", children: [
|
|
2994
3147
|
step !== "confirmation" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-widget-topbar", children: [
|
|
2995
|
-
/* @__PURE__ */ jsxRuntime.jsx("nav", { className: "resira-steps", "aria-label": "Booking steps", children: STEPS.filter((s) => s !== "confirmation").map((s, i) => {
|
|
3148
|
+
showStepIndicator && /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "resira-steps", "aria-label": "Booking steps", children: STEPS.filter((s) => s !== "confirmation").map((s, i) => {
|
|
2996
3149
|
const isCompleted = stepIndex(step, STEPS) > i;
|
|
2997
3150
|
const isActive = s === step;
|
|
2998
3151
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -3014,7 +3167,7 @@ function ResiraBookingWidget() {
|
|
|
3014
3167
|
stepSubtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-widget-subtitle", children: stepSubtitle })
|
|
3015
3168
|
] })
|
|
3016
3169
|
] }),
|
|
3017
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-widget-body", children: [
|
|
3170
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-widget-body", "aria-busy": loading || calendarLoading || resourcesLoading || productsLoading || footerBusy, children: [
|
|
3018
3171
|
step === "resource" && isServiceBased && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3019
3172
|
ProductSelector,
|
|
3020
3173
|
{
|
|
@@ -3037,10 +3190,10 @@ function ResiraBookingWidget() {
|
|
|
3037
3190
|
error: resourcesError
|
|
3038
3191
|
}
|
|
3039
3192
|
),
|
|
3040
|
-
step === "availability" && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: (loading || calendarLoading) && !availability && !calendarData ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", children: [
|
|
3041
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner" }),
|
|
3193
|
+
step === "availability" && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: (loading || calendarLoading) && !availability && !calendarData ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
|
|
3194
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
|
|
3042
3195
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-loading-text", children: locale.loading })
|
|
3043
|
-
] }) : error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", children: [
|
|
3196
|
+
] }) : error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", role: "alert", "aria-live": "assertive", children: [
|
|
3044
3197
|
/* @__PURE__ */ jsxRuntime.jsx(AlertCircleIcon, { size: 24 }),
|
|
3045
3198
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-error-message", children: error }),
|
|
3046
3199
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3511,12 +3664,17 @@ function supportsArQuickLook() {
|
|
|
3511
3664
|
const a = document.createElement("a");
|
|
3512
3665
|
return a.relList?.supports?.("ar") ?? false;
|
|
3513
3666
|
}
|
|
3667
|
+
function supportsModelViewer() {
|
|
3668
|
+
if (typeof window === "undefined") return false;
|
|
3669
|
+
return typeof customElements !== "undefined" && customElements.get("model-viewer") !== void 0;
|
|
3670
|
+
}
|
|
3514
3671
|
function ModelViewer3D({
|
|
3515
3672
|
dish,
|
|
3516
3673
|
onClose
|
|
3517
3674
|
}) {
|
|
3518
3675
|
const model = dish.model;
|
|
3519
3676
|
const arSupported = supportsArQuickLook();
|
|
3677
|
+
const modelViewerSupported = supportsModelViewer();
|
|
3520
3678
|
const arLinkRef = react.useRef(null);
|
|
3521
3679
|
const handleArClick = react.useCallback(() => {
|
|
3522
3680
|
arLinkRef.current?.click();
|
|
@@ -3538,7 +3696,7 @@ function ModelViewer3D({
|
|
|
3538
3696
|
),
|
|
3539
3697
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "resira-dish-viewer-title", children: dish.name })
|
|
3540
3698
|
] }),
|
|
3541
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-dish-viewer-canvas", children: model?.glbUrl ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3699
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-dish-viewer-canvas", children: model?.glbUrl && modelViewerSupported ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3542
3700
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3543
3701
|
"model-viewer",
|
|
3544
3702
|
{
|
|
@@ -3580,7 +3738,7 @@ function ModelViewer3D({
|
|
|
3580
3738
|
className: "resira-dish-viewer-fallback-img"
|
|
3581
3739
|
}
|
|
3582
3740
|
),
|
|
3583
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-dish-viewer-no3d", children: "No 3D model available" })
|
|
3741
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-dish-viewer-no3d", children: model?.glbUrl ? "3D preview is unavailable here. Showing image instead." : "No 3D model available" })
|
|
3584
3742
|
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-dish-viewer-fallback", children: [
|
|
3585
3743
|
/* @__PURE__ */ jsxRuntime.jsx(CubeIcon, { size: 48 }),
|
|
3586
3744
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-dish-viewer-no3d", children: "No preview available" })
|
|
@@ -3594,7 +3752,7 @@ function ModelViewer3D({
|
|
|
3594
3752
|
dish.allergens && dish.allergens.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-dish-viewer-allergens", children: dish.allergens.map((a) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-dish-viewer-allergen", children: a }, a)) })
|
|
3595
3753
|
] }),
|
|
3596
3754
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-dish-viewer-actions", children: [
|
|
3597
|
-
model?.glbUrl && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3755
|
+
model?.glbUrl && modelViewerSupported && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3598
3756
|
"button",
|
|
3599
3757
|
{
|
|
3600
3758
|
type: "button",
|
|
@@ -3673,7 +3831,8 @@ function DishShowcase({
|
|
|
3673
3831
|
const { dish: singleDish, loading: singleLoading, error: singleError } = useDish(
|
|
3674
3832
|
open && dishId ? dishId : void 0
|
|
3675
3833
|
);
|
|
3676
|
-
const
|
|
3834
|
+
const shouldLoadAllDishes = open && (!dishId || showAll);
|
|
3835
|
+
const { dishes: allDishes, loading: allLoading, error: allError } = useDishes(shouldLoadAllDishes);
|
|
3677
3836
|
const isSingleMode = !!dishId;
|
|
3678
3837
|
const loading = isSingleMode ? singleLoading : allLoading;
|
|
3679
3838
|
const error = isSingleMode ? singleError : allError;
|
|
@@ -3760,6 +3919,8 @@ function DishShowcase({
|
|
|
3760
3919
|
className: className || "resira-dish-showcase-btn",
|
|
3761
3920
|
style,
|
|
3762
3921
|
onClick: handleOpen,
|
|
3922
|
+
"aria-haspopup": "dialog",
|
|
3923
|
+
"aria-expanded": open,
|
|
3763
3924
|
children
|
|
3764
3925
|
}
|
|
3765
3926
|
),
|
|
@@ -3777,7 +3938,7 @@ function DishShowcase({
|
|
|
3777
3938
|
onClick: (e) => e.stopPropagation(),
|
|
3778
3939
|
role: "dialog",
|
|
3779
3940
|
"aria-modal": "true",
|
|
3780
|
-
"aria-label": "Dish showcase",
|
|
3941
|
+
"aria-label": selectedDish ? `${selectedDish.name} dish preview` : "Dish showcase",
|
|
3781
3942
|
children: [
|
|
3782
3943
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3783
3944
|
"button",
|
|
@@ -3789,11 +3950,11 @@ function DishShowcase({
|
|
|
3789
3950
|
children: /* @__PURE__ */ jsxRuntime.jsx(XIcon, { size: 18 })
|
|
3790
3951
|
}
|
|
3791
3952
|
),
|
|
3792
|
-
loading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-dish-loading", children: [
|
|
3793
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-dish-spinner" }),
|
|
3953
|
+
loading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-dish-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
|
|
3954
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-dish-spinner", "aria-hidden": "true" }),
|
|
3794
3955
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Loading dishes\u2026" })
|
|
3795
3956
|
] }),
|
|
3796
|
-
error && !loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-dish-error", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: error }) }),
|
|
3957
|
+
error && !loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-dish-error", role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: error }) }),
|
|
3797
3958
|
!loading && !error && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: selectedDish ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3798
3959
|
ModelViewer3D,
|
|
3799
3960
|
{
|