@cimplify/sdk 0.27.0 → 0.28.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/advanced.d.mts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/{chunk-MISITC47.mjs → chunk-FFUCCUG4.mjs} +47 -1
- package/dist/{chunk-4JSRPSNA.js → chunk-GS7AX6B3.js} +47 -0
- package/dist/{index-DBM89ktY.d.ts → index-C2U1gHyP.d.ts} +2 -2
- package/dist/{index-DT-24csu.d.mts → index-ClrQ0yTH.d.mts} +23 -2
- package/dist/{index-2JxW7TuQ.d.mts → index-EGhIxwfI.d.mts} +2 -2
- package/dist/{index-BAu-zmIP.d.ts → index-iHKILmG9.d.ts} +23 -2
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +13 -9
- package/dist/index.mjs +1 -1
- package/dist/{payment-5a21yzi7.d.ts → payment-k8feV7r8.d.mts} +55 -1
- package/dist/{payment-5a21yzi7.d.mts → payment-k8feV7r8.d.ts} +55 -1
- package/dist/{price-BS0Z5Oi9.d.ts → price-4EK-0zF5.d.ts} +1 -1
- package/dist/{price-CGPMlQUk.d.mts → price-Den0zv61.d.mts} +1 -1
- package/dist/react.d.mts +42 -4
- package/dist/react.d.ts +42 -4
- package/dist/react.js +360 -17
- package/dist/react.mjs +340 -3
- package/dist/utils.d.mts +3 -3
- package/dist/utils.d.ts +3 -3
- package/package.json +1 -1
- package/registry/product-card.json +1 -1
- package/registry/product-page.json +1 -1
package/dist/react.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { DURATION_UNIT, getVariantDisplayName, PRODUCT_TYPE, createCimplifyClient, RENDER_HINT } from './chunk-
|
|
3
|
-
export { getVariantDisplayName } from './chunk-
|
|
2
|
+
import { DURATION_UNIT, getVariantDisplayName, PRODUCT_TYPE, createCimplifyClient, RENDER_HINT } from './chunk-FFUCCUG4.mjs';
|
|
3
|
+
export { getVariantDisplayName } from './chunk-FFUCCUG4.mjs';
|
|
4
4
|
import { ELEMENT_TYPES, EVENT_TYPES } from './chunk-3J4PPSMI.mjs';
|
|
5
5
|
import { isSupportedCurrency, formatPrice, parsePrice, getUnitPriceAtQuantity, isOnSale, getDiscountPercentage, getBasePrice, getPriceRange, formatPriceRange } from './chunk-E3HC2YAE.mjs';
|
|
6
6
|
import React3, { createContext, useRef, useState, useCallback, useEffect, useContext, useMemo, useSyncExternalStore, useId } from 'react';
|
|
@@ -2847,6 +2847,84 @@ function useOrders(options = {}) {
|
|
|
2847
2847
|
const { data, isLoading, error, refetch } = ordersQuery(options);
|
|
2848
2848
|
return { orders: data ?? [], isLoading, error, refetch };
|
|
2849
2849
|
}
|
|
2850
|
+
function useSubscriptions(options = {}) {
|
|
2851
|
+
const { client } = useCimplifyClient();
|
|
2852
|
+
const { status, enabled = true } = options;
|
|
2853
|
+
const [subscriptions, setSubscriptions] = useState([]);
|
|
2854
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
2855
|
+
const [error, setError] = useState(null);
|
|
2856
|
+
const fetch2 = useCallback(async () => {
|
|
2857
|
+
if (!enabled) return;
|
|
2858
|
+
setIsLoading(true);
|
|
2859
|
+
setError(null);
|
|
2860
|
+
try {
|
|
2861
|
+
const result = await client.subscriptions.list();
|
|
2862
|
+
if (result.ok) {
|
|
2863
|
+
const filtered = status ? result.value.filter((s) => s.status === status) : result.value;
|
|
2864
|
+
setSubscriptions(filtered);
|
|
2865
|
+
} else {
|
|
2866
|
+
setError(result.error);
|
|
2867
|
+
}
|
|
2868
|
+
} catch (e) {
|
|
2869
|
+
setError(e);
|
|
2870
|
+
} finally {
|
|
2871
|
+
setIsLoading(false);
|
|
2872
|
+
}
|
|
2873
|
+
}, [client, enabled, status]);
|
|
2874
|
+
useEffect(() => {
|
|
2875
|
+
void fetch2();
|
|
2876
|
+
}, [fetch2]);
|
|
2877
|
+
return { subscriptions, isLoading, error, refetch: fetch2 };
|
|
2878
|
+
}
|
|
2879
|
+
function useSubscription(id, options = {}) {
|
|
2880
|
+
const { client } = useCimplifyClient();
|
|
2881
|
+
const { enabled = true } = options;
|
|
2882
|
+
const normalizedId = (id || "").trim();
|
|
2883
|
+
const [subscription, setSubscription] = useState(null);
|
|
2884
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
2885
|
+
const [error, setError] = useState(null);
|
|
2886
|
+
const fetch2 = useCallback(async () => {
|
|
2887
|
+
if (!enabled || !normalizedId) return;
|
|
2888
|
+
setIsLoading(true);
|
|
2889
|
+
setError(null);
|
|
2890
|
+
try {
|
|
2891
|
+
const result = await client.subscriptions.get(normalizedId);
|
|
2892
|
+
if (result.ok) {
|
|
2893
|
+
setSubscription(result.value);
|
|
2894
|
+
} else {
|
|
2895
|
+
setError(result.error);
|
|
2896
|
+
}
|
|
2897
|
+
} catch (e) {
|
|
2898
|
+
setError(e);
|
|
2899
|
+
} finally {
|
|
2900
|
+
setIsLoading(false);
|
|
2901
|
+
}
|
|
2902
|
+
}, [client, enabled, normalizedId]);
|
|
2903
|
+
useEffect(() => {
|
|
2904
|
+
void fetch2();
|
|
2905
|
+
}, [fetch2]);
|
|
2906
|
+
const cancel = useCallback(async (reason) => {
|
|
2907
|
+
if (!normalizedId) return;
|
|
2908
|
+
await client.subscriptions.cancel(normalizedId, reason);
|
|
2909
|
+
void fetch2();
|
|
2910
|
+
}, [client, normalizedId, fetch2]);
|
|
2911
|
+
const pause = useCallback(async () => {
|
|
2912
|
+
if (!normalizedId) return;
|
|
2913
|
+
await client.subscriptions.pause(normalizedId);
|
|
2914
|
+
void fetch2();
|
|
2915
|
+
}, [client, normalizedId, fetch2]);
|
|
2916
|
+
const resume = useCallback(async () => {
|
|
2917
|
+
if (!normalizedId) return;
|
|
2918
|
+
await client.subscriptions.resume(normalizedId);
|
|
2919
|
+
void fetch2();
|
|
2920
|
+
}, [client, normalizedId, fetch2]);
|
|
2921
|
+
const skipNext = useCallback(async () => {
|
|
2922
|
+
if (!normalizedId) return;
|
|
2923
|
+
await client.subscriptions.skipNextRenewal(normalizedId);
|
|
2924
|
+
void fetch2();
|
|
2925
|
+
}, [client, normalizedId, fetch2]);
|
|
2926
|
+
return { subscription, isLoading, error, cancel, pause, resume, skipNext, refetch: fetch2 };
|
|
2927
|
+
}
|
|
2850
2928
|
|
|
2851
2929
|
// src/react/hooks/use-services.ts
|
|
2852
2930
|
var servicesQuery = createCachedQuery({
|
|
@@ -5882,6 +5960,98 @@ function DigitalProductCard({
|
|
|
5882
5960
|
] })
|
|
5883
5961
|
] });
|
|
5884
5962
|
}
|
|
5963
|
+
function BundleProductCard({
|
|
5964
|
+
product,
|
|
5965
|
+
onQuickAdd,
|
|
5966
|
+
renderImage,
|
|
5967
|
+
renderLink,
|
|
5968
|
+
className
|
|
5969
|
+
}) {
|
|
5970
|
+
const image = product.image_url || product.images?.[0] || "";
|
|
5971
|
+
const components = product.components || [];
|
|
5972
|
+
const componentCount = components.length;
|
|
5973
|
+
const onSale = isOnSale(product);
|
|
5974
|
+
const discount = getDiscountPercentage(product);
|
|
5975
|
+
const hasVariantChoice = components.some((c) => c.allow_variant_choice);
|
|
5976
|
+
return /* @__PURE__ */ jsxs(CardShell, { product, renderLink, className, children: [
|
|
5977
|
+
/* @__PURE__ */ jsxs(CardImage, { src: image, alt: product.name, aspectRatio: "square", renderImage, children: [
|
|
5978
|
+
/* @__PURE__ */ jsx("span", { className: "absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-indigo-50 text-indigo-700 border border-indigo-200/60 px-2 py-0.5 rounded-md", children: "Bundle" }),
|
|
5979
|
+
componentCount > 0 && /* @__PURE__ */ jsxs("span", { className: "absolute top-3 right-3 text-[11px] font-semibold tracking-wide bg-background/90 backdrop-blur text-foreground px-2 py-0.5 rounded-md", children: [
|
|
5980
|
+
componentCount,
|
|
5981
|
+
" item",
|
|
5982
|
+
componentCount !== 1 ? "s" : ""
|
|
5983
|
+
] }),
|
|
5984
|
+
/* @__PURE__ */ jsx(
|
|
5985
|
+
QuickAddButton,
|
|
5986
|
+
{
|
|
5987
|
+
onClick: () => onQuickAdd?.(product),
|
|
5988
|
+
className: "absolute bottom-3 right-3"
|
|
5989
|
+
}
|
|
5990
|
+
)
|
|
5991
|
+
] }),
|
|
5992
|
+
/* @__PURE__ */ jsxs("div", { className: "p-3.5", children: [
|
|
5993
|
+
/* @__PURE__ */ jsx("h3", { className: "text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate", children: product.name }),
|
|
5994
|
+
componentCount > 0 && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-muted-foreground mt-1 truncate", children: [
|
|
5995
|
+
"Includes: ",
|
|
5996
|
+
components.map((c) => c.product_name || c.product_id).join(", ")
|
|
5997
|
+
] }),
|
|
5998
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2.5", children: [
|
|
5999
|
+
/* @__PURE__ */ jsx(Price, { amount: product.default_price, className: "text-sm font-bold" }),
|
|
6000
|
+
onSale && product.bundle_price && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6001
|
+
/* @__PURE__ */ jsx(Price, { amount: getBasePrice(product), className: "text-xs text-muted-foreground line-through" }),
|
|
6002
|
+
discount != null && discount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-semibold text-emerald-600", children: [
|
|
6003
|
+
"Save ",
|
|
6004
|
+
discount,
|
|
6005
|
+
"%"
|
|
6006
|
+
] })
|
|
6007
|
+
] })
|
|
6008
|
+
] }),
|
|
6009
|
+
hasVariantChoice && /* @__PURE__ */ jsx("span", { className: "inline-block mt-2 text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded", children: "Customizable" })
|
|
6010
|
+
] })
|
|
6011
|
+
] });
|
|
6012
|
+
}
|
|
6013
|
+
function CompositeProductCard({
|
|
6014
|
+
product,
|
|
6015
|
+
onQuickAdd,
|
|
6016
|
+
renderImage,
|
|
6017
|
+
renderLink,
|
|
6018
|
+
className
|
|
6019
|
+
}) {
|
|
6020
|
+
const image = product.image_url || product.images?.[0] || "";
|
|
6021
|
+
const groups = product.groups || [];
|
|
6022
|
+
const groupCount = groups.length;
|
|
6023
|
+
groups.reduce((sum, g) => sum + g.components.length, 0);
|
|
6024
|
+
const tags = product.tags || [];
|
|
6025
|
+
return /* @__PURE__ */ jsxs(CardShell, { product, renderLink, className, children: [
|
|
6026
|
+
/* @__PURE__ */ jsxs(CardImage, { src: image, alt: product.name, aspectRatio: "square", renderImage, children: [
|
|
6027
|
+
/* @__PURE__ */ jsx("span", { className: "absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-violet-50 text-violet-700 border border-violet-200/60 px-2 py-0.5 rounded-md", children: "Build Your Own" }),
|
|
6028
|
+
groupCount > 0 && /* @__PURE__ */ jsxs("span", { className: "absolute top-3 right-3 text-[11px] font-semibold tracking-wide bg-background/90 backdrop-blur text-foreground px-2 py-0.5 rounded-md", children: [
|
|
6029
|
+
groupCount,
|
|
6030
|
+
" choice",
|
|
6031
|
+
groupCount !== 1 ? "s" : ""
|
|
6032
|
+
] }),
|
|
6033
|
+
/* @__PURE__ */ jsx(
|
|
6034
|
+
QuickAddButton,
|
|
6035
|
+
{
|
|
6036
|
+
onClick: () => onQuickAdd?.(product),
|
|
6037
|
+
className: "absolute bottom-3 right-3"
|
|
6038
|
+
}
|
|
6039
|
+
)
|
|
6040
|
+
] }),
|
|
6041
|
+
/* @__PURE__ */ jsxs("div", { className: "p-3.5", children: [
|
|
6042
|
+
/* @__PURE__ */ jsx("h3", { className: "text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate", children: product.name }),
|
|
6043
|
+
product.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] text-muted-foreground mt-1 line-clamp-1", children: product.description }),
|
|
6044
|
+
groupCount > 0 && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground mt-1 truncate", children: groups.map((g) => g.name).join(" + ") }),
|
|
6045
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mt-2.5", children: [
|
|
6046
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm font-bold", children: [
|
|
6047
|
+
"From ",
|
|
6048
|
+
/* @__PURE__ */ jsx(Price, { amount: product.default_price })
|
|
6049
|
+
] }),
|
|
6050
|
+
tags.length > 0 && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded", children: tags[0] })
|
|
6051
|
+
] })
|
|
6052
|
+
] })
|
|
6053
|
+
] });
|
|
6054
|
+
}
|
|
5885
6055
|
function formatDuration2(minutes, unit) {
|
|
5886
6056
|
if (unit && unit !== "minutes") return `${minutes} ${unit}`;
|
|
5887
6057
|
if (minutes >= 60) {
|
|
@@ -6343,6 +6513,8 @@ var CardVariant = /* @__PURE__ */ ((CardVariant2) => {
|
|
|
6343
6513
|
CardVariant2["Retail"] = "retail";
|
|
6344
6514
|
CardVariant2["Wholesale"] = "wholesale";
|
|
6345
6515
|
CardVariant2["Digital"] = "digital";
|
|
6516
|
+
CardVariant2["Bundle"] = "bundle";
|
|
6517
|
+
CardVariant2["Composite"] = "composite";
|
|
6346
6518
|
CardVariant2["Standard"] = "standard";
|
|
6347
6519
|
CardVariant2["Compact"] = "compact";
|
|
6348
6520
|
CardVariant2["Schedule"] = "schedule";
|
|
@@ -6367,11 +6539,19 @@ var BUILT_IN_CARDS = {
|
|
|
6367
6539
|
["rental" /* Rental */]: RentalServiceCard,
|
|
6368
6540
|
["accommodation" /* Accommodation */]: AccommodationCard,
|
|
6369
6541
|
["lease" /* Lease */]: LeaseServiceCard,
|
|
6542
|
+
["bundle" /* Bundle */]: BundleProductCard,
|
|
6543
|
+
["composite" /* Composite */]: CompositeProductCard,
|
|
6370
6544
|
["subscription" /* Subscription */]: SubscriptionCard
|
|
6371
6545
|
};
|
|
6372
6546
|
var RENTAL_UNITS = /* @__PURE__ */ new Set([DURATION_UNIT.Days, DURATION_UNIT.Hours]);
|
|
6373
6547
|
var LEASE_UNITS = /* @__PURE__ */ new Set([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);
|
|
6374
6548
|
function resolveCardVariant(product) {
|
|
6549
|
+
if (product.type === PRODUCT_TYPE.Bundle) {
|
|
6550
|
+
return "bundle" /* Bundle */;
|
|
6551
|
+
}
|
|
6552
|
+
if (product.type === PRODUCT_TYPE.Composite) {
|
|
6553
|
+
return "composite" /* Composite */;
|
|
6554
|
+
}
|
|
6375
6555
|
if (product.quantity_pricing && product.quantity_pricing.length > 1) {
|
|
6376
6556
|
return "wholesale" /* Wholesale */;
|
|
6377
6557
|
}
|
|
@@ -8994,12 +9174,161 @@ function DigitalProductLayout({
|
|
|
8994
9174
|
)
|
|
8995
9175
|
] });
|
|
8996
9176
|
}
|
|
9177
|
+
function BundleProductLayout({
|
|
9178
|
+
product,
|
|
9179
|
+
onAddToCart,
|
|
9180
|
+
renderBreadcrumb,
|
|
9181
|
+
relatedProducts,
|
|
9182
|
+
showRelated = true,
|
|
9183
|
+
className
|
|
9184
|
+
}) {
|
|
9185
|
+
const images = product.images || (product.image_url ? [product.image_url] : []);
|
|
9186
|
+
const components = product.components || [];
|
|
9187
|
+
const onSale = isOnSale(product);
|
|
9188
|
+
const discount = getDiscountPercentage(product);
|
|
9189
|
+
const baseTotal = components.reduce(
|
|
9190
|
+
(sum, c) => sum + parsePrice(c.effective_price ?? 0) * (c.quantity ?? 1),
|
|
9191
|
+
0
|
|
9192
|
+
);
|
|
9193
|
+
const bundlePrice = parsePrice(product.default_price);
|
|
9194
|
+
const savings = baseTotal > bundlePrice ? baseTotal - bundlePrice : 0;
|
|
9195
|
+
return /* @__PURE__ */ jsxs("div", { "data-cimplify-product-layout": "bundle", className: cn("space-y-8", className), children: [
|
|
9196
|
+
renderBreadcrumb?.(product),
|
|
9197
|
+
/* @__PURE__ */ jsx(
|
|
9198
|
+
TwoColumnGrid,
|
|
9199
|
+
{
|
|
9200
|
+
left: /* @__PURE__ */ jsx(
|
|
9201
|
+
ProductImageGallery,
|
|
9202
|
+
{
|
|
9203
|
+
images,
|
|
9204
|
+
productName: product.name
|
|
9205
|
+
}
|
|
9206
|
+
),
|
|
9207
|
+
right: /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
9208
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
9209
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-indigo-700 bg-indigo-50 border border-indigo-200/60 px-2.5 py-1 rounded-md", children: "Bundle" }),
|
|
9210
|
+
savings > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200/60 px-2.5 py-1 rounded-md", children: [
|
|
9211
|
+
"Save ",
|
|
9212
|
+
discount,
|
|
9213
|
+
"%"
|
|
9214
|
+
] }),
|
|
9215
|
+
/* @__PURE__ */ jsx(TagPills, { tags: product.tags || [] })
|
|
9216
|
+
] }),
|
|
9217
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
9218
|
+
/* @__PURE__ */ jsx("h1", { "data-cimplify-product-layout-name": true, className: "text-3xl lg:text-4xl font-bold tracking-tight", children: product.name }),
|
|
9219
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-3 mt-2", children: [
|
|
9220
|
+
/* @__PURE__ */ jsx(Price, { amount: product.default_price, className: "text-2xl font-bold" }),
|
|
9221
|
+
onSale && baseTotal > 0 && /* @__PURE__ */ jsx(Price, { amount: baseTotal, className: "text-lg text-muted-foreground line-through" })
|
|
9222
|
+
] })
|
|
9223
|
+
] }),
|
|
9224
|
+
product.description && /* @__PURE__ */ jsx("p", { "data-cimplify-product-layout-description": true, className: "text-muted-foreground leading-relaxed", children: product.description }),
|
|
9225
|
+
components.length > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-bundle-components": true, className: "space-y-3", children: [
|
|
9226
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Included in this bundle" }),
|
|
9227
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y divide-border border border-border rounded-lg overflow-hidden", children: components.map((component) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3", children: [
|
|
9228
|
+
component.product_image_url && /* @__PURE__ */ jsx("div", { className: "w-12 h-12 rounded-lg overflow-hidden bg-muted shrink-0", children: /* @__PURE__ */ jsx("img", { src: component.product_image_url, alt: component.product_name, className: "w-full h-full object-cover" }) }),
|
|
9229
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
9230
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", children: component.product_name }),
|
|
9231
|
+
component.quantity > 1 && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
9232
|
+
"Qty: ",
|
|
9233
|
+
component.quantity
|
|
9234
|
+
] }),
|
|
9235
|
+
component.allow_variant_choice && /* @__PURE__ */ jsx("p", { className: "text-xs text-violet-600 font-medium", children: "Choose variant" })
|
|
9236
|
+
] }),
|
|
9237
|
+
/* @__PURE__ */ jsx(Price, { amount: component.effective_price ?? 0, className: "text-sm text-muted-foreground shrink-0" })
|
|
9238
|
+
] }, component.id)) })
|
|
9239
|
+
] }),
|
|
9240
|
+
savings > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-bundle-savings": true, className: "flex items-center gap-2 p-3 bg-emerald-50 border border-emerald-200/60 text-sm text-emerald-800", children: [
|
|
9241
|
+
/* @__PURE__ */ jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }),
|
|
9242
|
+
"You save ",
|
|
9243
|
+
/* @__PURE__ */ jsx(Price, { amount: savings, className: "font-semibold" }),
|
|
9244
|
+
" with this bundle"
|
|
9245
|
+
] }),
|
|
9246
|
+
/* @__PURE__ */ jsx(CustomAttributesTable, { attributes: product.custom_attributes || [] }),
|
|
9247
|
+
/* @__PURE__ */ jsx(
|
|
9248
|
+
ProductCustomizer,
|
|
9249
|
+
{
|
|
9250
|
+
product,
|
|
9251
|
+
onAddToCart
|
|
9252
|
+
}
|
|
9253
|
+
)
|
|
9254
|
+
] })
|
|
9255
|
+
}
|
|
9256
|
+
),
|
|
9257
|
+
showRelated && relatedProducts && relatedProducts.length > 0 && /* @__PURE__ */ jsx(RelatedProductsSection, { products: relatedProducts })
|
|
9258
|
+
] });
|
|
9259
|
+
}
|
|
9260
|
+
function CompositeProductLayout({
|
|
9261
|
+
product,
|
|
9262
|
+
onAddToCart,
|
|
9263
|
+
renderBreadcrumb,
|
|
9264
|
+
relatedProducts,
|
|
9265
|
+
showRelated = true,
|
|
9266
|
+
className
|
|
9267
|
+
}) {
|
|
9268
|
+
const images = product.images || (product.image_url ? [product.image_url] : []);
|
|
9269
|
+
const groups = product.groups || [];
|
|
9270
|
+
return /* @__PURE__ */ jsxs("div", { "data-cimplify-product-layout": "composite", className: cn("space-y-8", className), children: [
|
|
9271
|
+
renderBreadcrumb?.(product),
|
|
9272
|
+
/* @__PURE__ */ jsx(
|
|
9273
|
+
TwoColumnGrid,
|
|
9274
|
+
{
|
|
9275
|
+
left: /* @__PURE__ */ jsx(
|
|
9276
|
+
ProductImageGallery,
|
|
9277
|
+
{
|
|
9278
|
+
images,
|
|
9279
|
+
productName: product.name
|
|
9280
|
+
}
|
|
9281
|
+
),
|
|
9282
|
+
right: /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
9283
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
9284
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-violet-700 bg-violet-50 border border-violet-200/60 px-2.5 py-1 rounded-md", children: "Build Your Own" }),
|
|
9285
|
+
groups.length > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-muted-foreground bg-muted px-2.5 py-1 rounded-md", children: [
|
|
9286
|
+
groups.length,
|
|
9287
|
+
" choice",
|
|
9288
|
+
groups.length !== 1 ? "s" : ""
|
|
9289
|
+
] }),
|
|
9290
|
+
/* @__PURE__ */ jsx(TagPills, { tags: product.tags || [] })
|
|
9291
|
+
] }),
|
|
9292
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
9293
|
+
/* @__PURE__ */ jsx("h1", { "data-cimplify-product-layout-name": true, className: "text-3xl lg:text-4xl font-bold tracking-tight", children: product.name }),
|
|
9294
|
+
/* @__PURE__ */ jsxs("p", { className: "text-lg text-muted-foreground mt-2", children: [
|
|
9295
|
+
"From ",
|
|
9296
|
+
/* @__PURE__ */ jsx(Price, { amount: product.default_price, className: "font-semibold text-foreground" })
|
|
9297
|
+
] })
|
|
9298
|
+
] }),
|
|
9299
|
+
product.description && /* @__PURE__ */ jsx("p", { "data-cimplify-product-layout-description": true, className: "text-muted-foreground leading-relaxed", children: product.description }),
|
|
9300
|
+
groups.length > 0 && /* @__PURE__ */ jsx("div", { "data-cimplify-composite-overview": true, className: "flex flex-wrap gap-2", children: groups.map((group) => {
|
|
9301
|
+
const availableCount = group.components.filter((c) => c.is_available && !c.is_archived).length;
|
|
9302
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted rounded-lg text-sm", children: [
|
|
9303
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: group.name }),
|
|
9304
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
|
|
9305
|
+
group.min_selections > 0 ? `Pick ${group.min_selections}` : "Optional",
|
|
9306
|
+
` \xB7 ${availableCount} option${availableCount !== 1 ? "s" : ""}`
|
|
9307
|
+
] })
|
|
9308
|
+
] }, group.id);
|
|
9309
|
+
}) }),
|
|
9310
|
+
/* @__PURE__ */ jsx(CustomAttributesTable, { attributes: product.custom_attributes || [] }),
|
|
9311
|
+
/* @__PURE__ */ jsx(
|
|
9312
|
+
ProductCustomizer,
|
|
9313
|
+
{
|
|
9314
|
+
product,
|
|
9315
|
+
onAddToCart
|
|
9316
|
+
}
|
|
9317
|
+
)
|
|
9318
|
+
] })
|
|
9319
|
+
}
|
|
9320
|
+
),
|
|
9321
|
+
showRelated && relatedProducts && relatedProducts.length > 0 && /* @__PURE__ */ jsx(RelatedProductsSection, { products: relatedProducts })
|
|
9322
|
+
] });
|
|
9323
|
+
}
|
|
8997
9324
|
var ProductTemplate = /* @__PURE__ */ ((ProductTemplate2) => {
|
|
8998
9325
|
ProductTemplate2["Default"] = "default";
|
|
8999
9326
|
ProductTemplate2["Food"] = "food";
|
|
9000
9327
|
ProductTemplate2["Wholesale"] = "wholesale";
|
|
9001
9328
|
ProductTemplate2["Service"] = "service";
|
|
9002
9329
|
ProductTemplate2["Digital"] = "digital";
|
|
9330
|
+
ProductTemplate2["Bundle"] = "bundle";
|
|
9331
|
+
ProductTemplate2["Composite"] = "composite";
|
|
9003
9332
|
ProductTemplate2["Physical"] = "physical";
|
|
9004
9333
|
return ProductTemplate2;
|
|
9005
9334
|
})(ProductTemplate || {});
|
|
@@ -9008,6 +9337,8 @@ var BUILT_IN_LAYOUTS2 = {
|
|
|
9008
9337
|
["wholesale" /* Wholesale */]: WholesaleProductLayout,
|
|
9009
9338
|
["service" /* Service */]: ServiceProductLayout,
|
|
9010
9339
|
["digital" /* Digital */]: DigitalProductLayout,
|
|
9340
|
+
["bundle" /* Bundle */]: BundleProductLayout,
|
|
9341
|
+
["composite" /* Composite */]: CompositeProductLayout,
|
|
9011
9342
|
["default" /* Default */]: DefaultProductLayout
|
|
9012
9343
|
};
|
|
9013
9344
|
function resolveTemplateKey(product) {
|
|
@@ -9015,6 +9346,12 @@ function resolveTemplateKey(product) {
|
|
|
9015
9346
|
if (typeof metaTemplate === "string" && metaTemplate.trim()) {
|
|
9016
9347
|
return metaTemplate.trim();
|
|
9017
9348
|
}
|
|
9349
|
+
if (product.type === PRODUCT_TYPE.Bundle) {
|
|
9350
|
+
return "bundle" /* Bundle */;
|
|
9351
|
+
}
|
|
9352
|
+
if (product.type === PRODUCT_TYPE.Composite) {
|
|
9353
|
+
return "composite" /* Composite */;
|
|
9354
|
+
}
|
|
9018
9355
|
if (product.quantity_pricing && product.quantity_pricing.length > 1) {
|
|
9019
9356
|
return "wholesale" /* Wholesale */;
|
|
9020
9357
|
}
|
|
@@ -10603,4 +10940,4 @@ function ResourcePicker({
|
|
|
10603
10940
|
);
|
|
10604
10941
|
}
|
|
10605
10942
|
|
|
10606
|
-
export { AccommodationCard, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BillingPlanSelector, BookingCard, BookingList, BundleSelector, CardImage, CardShell, CardVariant, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, CheckoutPage, CimplifyAccount, CimplifyCheckout, CimplifyProvider, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeSelector, CurrencySelector, CustomAttributesTable, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, FeaturedCollectionLayout, FoodProductCard, FoodProductLayout, InventoryBadge, LeaseServiceCard, LocationPicker, MetadataStringList, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderSummary, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, SaleBadge, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, ServiceProductLayout, SessionMessageBanner, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, StoreNav, SubscriptionCard, TagPills, TwoColumnGrid, VariantSelector, VolumePricing, WholesaleProductCard, WholesaleProductLayout, WishlistButton, cn, roomToResource, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCategories, useCheckout, useCimplify, useCimplifyClient, useCollection, useCollections, useDeals, useDeliveryFee, useElements, useElementsReady, useFxRate, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
|
|
10943
|
+
export { AccommodationCard, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BillingPlanSelector, BookingCard, BookingList, BundleProductCard, BundleProductLayout, BundleSelector, CardImage, CardShell, CardVariant, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, CheckoutPage, CimplifyAccount, CimplifyCheckout, CimplifyProvider, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, CurrencySelector, CustomAttributesTable, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, FeaturedCollectionLayout, FoodProductCard, FoodProductLayout, InventoryBadge, LeaseServiceCard, LocationPicker, MetadataStringList, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderSummary, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, SaleBadge, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, ServiceProductLayout, SessionMessageBanner, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, StoreNav, SubscriptionCard, TagPills, TwoColumnGrid, VariantSelector, VolumePricing, WholesaleProductCard, WholesaleProductLayout, WishlistButton, cn, roomToResource, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCategories, useCheckout, useCimplify, useCimplifyClient, useCollection, useCollections, useDeals, useDeliveryFee, useElements, useElementsReady, useFxRate, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-
|
|
2
|
-
import './payment-
|
|
3
|
-
import './price-
|
|
1
|
+
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-EGhIxwfI.mjs';
|
|
2
|
+
import './payment-k8feV7r8.mjs';
|
|
3
|
+
import './price-Den0zv61.mjs';
|
|
4
4
|
import 'libphonenumber-js';
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-
|
|
2
|
-
import './payment-
|
|
3
|
-
import './price-
|
|
1
|
+
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-C2U1gHyP.js';
|
|
2
|
+
import './payment-k8feV7r8.js';
|
|
3
|
+
import './price-4EK-0zF5.js';
|
|
4
4
|
import 'libphonenumber-js';
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "product-card.tsx",
|
|
14
|
-
"content": "\"use client\";\n\nimport React, { useCallback, useState } from \"react\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from \"@cimplify/sdk\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { CardVariant } from \"@cimplify/sdk/react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { FoodProductCard } from \"@cimplify/sdk/react\";\nimport { RetailProductCard } from \"@cimplify/sdk/react\";\nimport { WholesaleProductCard } from \"@cimplify/sdk/react\";\nimport { DigitalProductCard } from \"@cimplify/sdk/react\";\nimport { StandardServiceCard } from \"@cimplify/sdk/react\";\nimport { RentalServiceCard } from \"@cimplify/sdk/react\";\nimport { AccommodationCard } from \"@cimplify/sdk/react\";\nimport { LeaseServiceCard } from \"@cimplify/sdk/react\";\nimport { SubscriptionCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Explicit card variant. Auto-detected from product data when omitted. */\n variant?: CardVariant;\n /** Per-slug card map. Highest priority — maps a product slug to a custom card component. */\n cards?: Record<string, React.ComponentType<CardLayoutProps>>;\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Called when quick-add button is clicked (if the resolved card supports it). */\n onQuickAdd?: (product: Product) => void;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\nconst BUILT_IN_CARDS: Record<string, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nfunction resolveCardVariant(product: Product): CardVariant {\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return CardVariant.Wholesale;\n }\n if (product.type === PRODUCT_TYPE.Digital) {\n return CardVariant.Digital;\n }\n if (product.type === PRODUCT_TYPE.Service) {\n if (product.duration_unit && RENTAL_UNITS.has(product.duration_unit)) {\n return CardVariant.Rental;\n }\n if (product.duration_unit === DURATION_UNIT.Nights) {\n return CardVariant.Accommodation;\n }\n if (product.duration_unit && LEASE_UNITS.has(product.duration_unit)) {\n return CardVariant.Lease;\n }\n if (product.billing_plans && product.billing_plans.length > 0 && !product.duration_minutes) {\n return CardVariant.Subscription;\n }\n return CardVariant.Standard;\n }\n if (product.render_hint === RENDER_HINT.Food) {\n return CardVariant.Food;\n }\n if (product.render_hint === RENDER_HINT.Physical) {\n return CardVariant.Retail;\n }\n return CardVariant.Retail;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n variant,\n cards,\n href,\n renderModal,\n renderImage,\n renderLink,\n onQuickAdd,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = (() => {\n if (children) return children;\n\n // Resolve card variant: slug map → explicit variant → auto-detect\n const SlugCard = cards?.[product.slug];\n if (SlugCard) {\n return <SlugCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n const key = variant ?? resolveCardVariant(product);\n const ResolvedCard = BUILT_IN_CARDS[key];\n if (ResolvedCard) {\n return <ResolvedCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n // Fallback: minimal default\n return (\n <>\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={cn(\"overflow-hidden rounded-t-xl\", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({ src: imageUrl, alt: product.name, className: classNames?.image })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={cn(\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\", classNames?.image)}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n <div data-cimplify-product-card-body className={cn(\"p-4 flex flex-col gap-1\", classNames?.body)}>\n <span data-cimplify-product-card-name className={cn(\"font-semibold text-foreground leading-tight\", classNames?.name)}>\n {product.name}\n </span>\n {product.description && (\n <span data-cimplify-product-card-description className={cn(\"text-sm text-muted-foreground line-clamp-2\", classNames?.description)}>\n {product.description}\n </span>\n )}\n <Price amount={product.default_price} className={cn(\"text-sm font-medium text-foreground mt-1\", classNames?.price)} />\n </div>\n </>\n );\n })();\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"group block no-underline text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(\n \"group block w-full text-left p-0 cursor-pointer font-[inherit] text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity\",\n classNames?.modalOverlay,\n )}\n />\n <Dialog.Popup\n data-cimplify-product-card-modal\n className={cn(\n \"fixed z-50 rounded-2xl p-0 max-w-lg w-full h-fit max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none\",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails, handleClose)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy=\"true\"\n className=\"flex flex-col\"\n >\n <div className=\"aspect-[5/2] bg-muted animate-pulse\" />\n <div className=\"p-6 space-y-3\">\n <div className=\"h-5 w-3/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-7 w-2/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-4 w-4/5 bg-muted rounded animate-pulse\" />\n </div>\n <div className=\"mx-6 border-t border-border pt-4 pb-6\">\n <div className=\"h-14 bg-muted rounded-full animate-pulse\" />\n </div>\n </div>\n )\n )}\n </Dialog.Popup>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n"
|
|
14
|
+
"content": "\"use client\";\n\nimport React, { useCallback, useState } from \"react\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from \"@cimplify/sdk\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { CardVariant } from \"@cimplify/sdk/react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { FoodProductCard } from \"@cimplify/sdk/react\";\nimport { RetailProductCard } from \"@cimplify/sdk/react\";\nimport { WholesaleProductCard } from \"@cimplify/sdk/react\";\nimport { DigitalProductCard } from \"@cimplify/sdk/react\";\nimport { StandardServiceCard } from \"@cimplify/sdk/react\";\nimport { RentalServiceCard } from \"@cimplify/sdk/react\";\nimport { AccommodationCard } from \"@cimplify/sdk/react\";\nimport { LeaseServiceCard } from \"@cimplify/sdk/react\";\nimport { SubscriptionCard } from \"@cimplify/sdk/react\";\nimport { BundleProductCard } from \"./cards/bundle-product-card\";\nimport { CompositeProductCard } from \"./cards/composite-product-card\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Explicit card variant. Auto-detected from product data when omitted. */\n variant?: CardVariant;\n /** Per-slug card map. Highest priority — maps a product slug to a custom card component. */\n cards?: Record<string, React.ComponentType<CardLayoutProps>>;\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Called when quick-add button is clicked (if the resolved card supports it). */\n onQuickAdd?: (product: Product) => void;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\nconst BUILT_IN_CARDS: Record<string, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nfunction resolveCardVariant(product: Product): CardVariant {\n if (product.type === PRODUCT_TYPE.Bundle) {\n return CardVariant.Bundle;\n }\n if (product.type === PRODUCT_TYPE.Composite) {\n return CardVariant.Composite;\n }\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return CardVariant.Wholesale;\n }\n if (product.type === PRODUCT_TYPE.Digital) {\n return CardVariant.Digital;\n }\n if (product.type === PRODUCT_TYPE.Service) {\n if (product.duration_unit && RENTAL_UNITS.has(product.duration_unit)) {\n return CardVariant.Rental;\n }\n if (product.duration_unit === DURATION_UNIT.Nights) {\n return CardVariant.Accommodation;\n }\n if (product.duration_unit && LEASE_UNITS.has(product.duration_unit)) {\n return CardVariant.Lease;\n }\n if (product.billing_plans && product.billing_plans.length > 0 && !product.duration_minutes) {\n return CardVariant.Subscription;\n }\n return CardVariant.Standard;\n }\n if (product.render_hint === RENDER_HINT.Food) {\n return CardVariant.Food;\n }\n if (product.render_hint === RENDER_HINT.Physical) {\n return CardVariant.Retail;\n }\n return CardVariant.Retail;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n variant,\n cards,\n href,\n renderModal,\n renderImage,\n renderLink,\n onQuickAdd,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = (() => {\n if (children) return children;\n\n // Resolve card variant: slug map → explicit variant → auto-detect\n const SlugCard = cards?.[product.slug];\n if (SlugCard) {\n return <SlugCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n const key = variant ?? resolveCardVariant(product);\n const ResolvedCard = BUILT_IN_CARDS[key];\n if (ResolvedCard) {\n return <ResolvedCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n // Fallback: minimal default\n return (\n <>\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={cn(\"overflow-hidden rounded-t-xl\", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({ src: imageUrl, alt: product.name, className: classNames?.image })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={cn(\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\", classNames?.image)}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n <div data-cimplify-product-card-body className={cn(\"p-4 flex flex-col gap-1\", classNames?.body)}>\n <span data-cimplify-product-card-name className={cn(\"font-semibold text-foreground leading-tight\", classNames?.name)}>\n {product.name}\n </span>\n {product.description && (\n <span data-cimplify-product-card-description className={cn(\"text-sm text-muted-foreground line-clamp-2\", classNames?.description)}>\n {product.description}\n </span>\n )}\n <Price amount={product.default_price} className={cn(\"text-sm font-medium text-foreground mt-1\", classNames?.price)} />\n </div>\n </>\n );\n })();\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"group block no-underline text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(\n \"group block w-full text-left p-0 cursor-pointer font-[inherit] text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity\",\n classNames?.modalOverlay,\n )}\n />\n <Dialog.Popup\n data-cimplify-product-card-modal\n className={cn(\n \"fixed z-50 rounded-2xl p-0 max-w-lg w-full h-fit max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none\",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails, handleClose)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy=\"true\"\n className=\"flex flex-col\"\n >\n <div className=\"aspect-[5/2] bg-muted animate-pulse\" />\n <div className=\"p-6 space-y-3\">\n <div className=\"h-5 w-3/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-7 w-2/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-4 w-4/5 bg-muted rounded animate-pulse\" />\n </div>\n <div className=\"mx-6 border-t border-border pt-4 pb-6\">\n <div className=\"h-14 bg-muted rounded-full animate-pulse\" />\n </div>\n </div>\n )\n )}\n </Dialog.Popup>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "product-page.tsx",
|
|
17
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === PRODUCT_TYPE.Service) {\n return ProductTemplate.Service;\n }\n\n if (product.type === PRODUCT_TYPE.Digital) {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === RENDER_HINT.Food) {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === RENDER_HINT.Physical) {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || \"\";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\n });\n const product = productProp ?? fetched;\n\n if (isLoading && !product) {\n return (\n <div\n data-cimplify-product-page\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div className=\"grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse\">\n <div className=\"aspect-square bg-muted\" />\n <div className=\"space-y-4 py-4\">\n <div className=\"h-8 w-3/5 bg-muted\" />\n <div className=\"h-6 w-2/5 bg-muted\" />\n <div className=\"h-4 w-4/5 bg-muted\" />\n <div className=\"h-4 w-3/5 bg-muted\" />\n <div className=\"h-14 w-full bg-muted mt-8\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (!product) {\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <p className=\"text-muted-foreground\">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n"
|
|
17
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport { BundleProductLayout } from \"./layouts/bundle-product-layout\";\nimport { CompositeProductLayout } from \"./layouts/composite-product-layout\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Bundle = \"bundle\",\n Composite = \"composite\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Bundle]: BundleProductLayout,\n [ProductTemplate.Composite]: CompositeProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.type === PRODUCT_TYPE.Bundle) {\n return ProductTemplate.Bundle;\n }\n\n if (product.type === PRODUCT_TYPE.Composite) {\n return ProductTemplate.Composite;\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === PRODUCT_TYPE.Service) {\n return ProductTemplate.Service;\n }\n\n if (product.type === PRODUCT_TYPE.Digital) {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === RENDER_HINT.Food) {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === RENDER_HINT.Physical) {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || \"\";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\n });\n const product = productProp ?? fetched;\n\n if (isLoading && !product) {\n return (\n <div\n data-cimplify-product-page\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div className=\"grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse\">\n <div className=\"aspect-square bg-muted\" />\n <div className=\"space-y-4 py-4\">\n <div className=\"h-8 w-3/5 bg-muted\" />\n <div className=\"h-6 w-2/5 bg-muted\" />\n <div className=\"h-4 w-4/5 bg-muted\" />\n <div className=\"h-4 w-3/5 bg-muted\" />\n <div className=\"h-14 w-full bg-muted mt-8\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (!product) {\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <p className=\"text-muted-foreground\">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|