@cimplify/sdk 0.60.0 → 0.62.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/react.d.mts +20 -1
- package/dist/react.d.ts +20 -1
- package/dist/react.js +159 -11
- package/dist/react.mjs +158 -12
- package/dist/styles.css +1 -1
- package/dist/testing.d.mts +15 -15
- package/dist/testing.d.ts +15 -15
- package/package.json +2 -1
- package/registry/default-product-layout.json +2 -1
- package/registry/digital-product-layout.json +2 -1
- package/registry/food-product-layout.json +2 -1
- package/registry/index.json +15 -0
- package/registry/product-sheet.json +2 -1
- package/registry/rich-text.json +15 -0
- package/registry/service-product-layout.json +2 -1
- package/registry/wholesale-product-layout.json +2 -1
package/dist/testing.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ declare const CountryCodeSchema: z.ZodString;
|
|
|
54
54
|
declare const LocaleSchema: z.ZodString;
|
|
55
55
|
declare const PhoneE164Schema: z.ZodString;
|
|
56
56
|
declare const SeedNameSchema: z.ZodEnum<{
|
|
57
|
-
default: "default";
|
|
58
57
|
empty: "empty";
|
|
58
|
+
default: "default";
|
|
59
59
|
"reesa-storefront": "reesa-storefront";
|
|
60
60
|
restaurant: "restaurant";
|
|
61
61
|
retail: "retail";
|
|
@@ -71,11 +71,11 @@ declare const ProductTypeSchema: z.ZodEnum<{
|
|
|
71
71
|
composite: "composite";
|
|
72
72
|
}>;
|
|
73
73
|
declare const LineTypeSchema: z.ZodEnum<{
|
|
74
|
+
simple: "simple";
|
|
74
75
|
service: "service";
|
|
75
76
|
digital: "digital";
|
|
76
77
|
bundle: "bundle";
|
|
77
78
|
composite: "composite";
|
|
78
|
-
simple: "simple";
|
|
79
79
|
}>;
|
|
80
80
|
declare const ServiceStatusSchema: z.ZodEnum<{
|
|
81
81
|
pending: "pending";
|
|
@@ -365,8 +365,8 @@ declare const BrandSchema: z.ZodObject<{
|
|
|
365
365
|
}, z.core.$strip>;
|
|
366
366
|
mock: z.ZodObject<{
|
|
367
367
|
seed: z.ZodEnum<{
|
|
368
|
-
default: "default";
|
|
369
368
|
empty: "empty";
|
|
369
|
+
default: "default";
|
|
370
370
|
"reesa-storefront": "reesa-storefront";
|
|
371
371
|
restaurant: "restaurant";
|
|
372
372
|
retail: "retail";
|
|
@@ -531,7 +531,7 @@ declare const assertBrand: (value: unknown) => asserts value is {
|
|
|
531
531
|
summary: string;
|
|
532
532
|
};
|
|
533
533
|
mock: {
|
|
534
|
-
seed: "
|
|
534
|
+
seed: "empty" | "default" | "reesa-storefront" | "restaurant" | "retail" | "services" | "grocery" | "fashion";
|
|
535
535
|
businessId: string;
|
|
536
536
|
};
|
|
537
537
|
trustItems?: {
|
|
@@ -780,11 +780,11 @@ declare const CartItemSchema: z.ZodObject<{
|
|
|
780
780
|
quantity: z.ZodNumber;
|
|
781
781
|
line_key: z.ZodString;
|
|
782
782
|
line_type: z.ZodEnum<{
|
|
783
|
+
simple: "simple";
|
|
783
784
|
service: "service";
|
|
784
785
|
digital: "digital";
|
|
785
786
|
bundle: "bundle";
|
|
786
787
|
composite: "composite";
|
|
787
|
-
simple: "simple";
|
|
788
788
|
}>;
|
|
789
789
|
name: z.ZodString;
|
|
790
790
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -926,7 +926,7 @@ declare const assertCartItem: (value: unknown) => asserts value is {
|
|
|
926
926
|
item_id: string;
|
|
927
927
|
quantity: number;
|
|
928
928
|
line_key: string;
|
|
929
|
-
line_type: "
|
|
929
|
+
line_type: "simple" | "service" | "digital" | "bundle" | "composite";
|
|
930
930
|
name: string;
|
|
931
931
|
is_available: boolean;
|
|
932
932
|
base_price: string;
|
|
@@ -1076,11 +1076,11 @@ declare const CartSchema: z.ZodObject<{
|
|
|
1076
1076
|
quantity: z.ZodNumber;
|
|
1077
1077
|
line_key: z.ZodString;
|
|
1078
1078
|
line_type: z.ZodEnum<{
|
|
1079
|
+
simple: "simple";
|
|
1079
1080
|
service: "service";
|
|
1080
1081
|
digital: "digital";
|
|
1081
1082
|
bundle: "bundle";
|
|
1082
1083
|
composite: "composite";
|
|
1083
|
-
simple: "simple";
|
|
1084
1084
|
}>;
|
|
1085
1085
|
name: z.ZodString;
|
|
1086
1086
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -1249,7 +1249,7 @@ declare const assertCart: (value: unknown) => asserts value is {
|
|
|
1249
1249
|
item_id: string;
|
|
1250
1250
|
quantity: number;
|
|
1251
1251
|
line_key: string;
|
|
1252
|
-
line_type: "
|
|
1252
|
+
line_type: "simple" | "service" | "digital" | "bundle" | "composite";
|
|
1253
1253
|
name: string;
|
|
1254
1254
|
is_available: boolean;
|
|
1255
1255
|
base_price: string;
|
|
@@ -1462,10 +1462,10 @@ declare const assertCheckoutBody: (value: unknown) => asserts value is {
|
|
|
1462
1462
|
};
|
|
1463
1463
|
declare const AuthorizationTypeSchema: z.ZodEnum<{
|
|
1464
1464
|
phone: "phone";
|
|
1465
|
-
address: "address";
|
|
1466
1465
|
otp: "otp";
|
|
1467
1466
|
pin: "pin";
|
|
1468
1467
|
birthday: "birthday";
|
|
1468
|
+
address: "address";
|
|
1469
1469
|
}>;
|
|
1470
1470
|
declare const NextActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1471
1471
|
type: z.ZodLiteral<"none">;
|
|
@@ -1485,10 +1485,10 @@ declare const CheckoutResponseSchema: z.ZodObject<{
|
|
|
1485
1485
|
requires_authorization: z.ZodBoolean;
|
|
1486
1486
|
authorization_type: z.ZodOptional<z.ZodEnum<{
|
|
1487
1487
|
phone: "phone";
|
|
1488
|
-
address: "address";
|
|
1489
1488
|
otp: "otp";
|
|
1490
1489
|
pin: "pin";
|
|
1491
1490
|
birthday: "birthday";
|
|
1491
|
+
address: "address";
|
|
1492
1492
|
}>>;
|
|
1493
1493
|
next_action: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1494
1494
|
type: z.ZodLiteral<"none">;
|
|
@@ -1525,7 +1525,7 @@ declare const assertCheckoutResponse: (value: unknown) => asserts value is {
|
|
|
1525
1525
|
display_text?: string | undefined;
|
|
1526
1526
|
};
|
|
1527
1527
|
payment_reference?: string | undefined;
|
|
1528
|
-
authorization_type?: "phone" | "
|
|
1528
|
+
authorization_type?: "phone" | "otp" | "pin" | "birthday" | "address" | undefined;
|
|
1529
1529
|
provider?: string | undefined;
|
|
1530
1530
|
authorization_url?: string | undefined;
|
|
1531
1531
|
display_text?: string | undefined;
|
|
@@ -1724,8 +1724,8 @@ declare const PUBLIC_SCHEMAS: {
|
|
|
1724
1724
|
}, z.core.$strip>;
|
|
1725
1725
|
mock: z.ZodObject<{
|
|
1726
1726
|
seed: z.ZodEnum<{
|
|
1727
|
-
default: "default";
|
|
1728
1727
|
empty: "empty";
|
|
1728
|
+
default: "default";
|
|
1729
1729
|
"reesa-storefront": "reesa-storefront";
|
|
1730
1730
|
restaurant: "restaurant";
|
|
1731
1731
|
retail: "retail";
|
|
@@ -1785,11 +1785,11 @@ declare const PUBLIC_SCHEMAS: {
|
|
|
1785
1785
|
quantity: z.ZodNumber;
|
|
1786
1786
|
line_key: z.ZodString;
|
|
1787
1787
|
line_type: z.ZodEnum<{
|
|
1788
|
+
simple: "simple";
|
|
1788
1789
|
service: "service";
|
|
1789
1790
|
digital: "digital";
|
|
1790
1791
|
bundle: "bundle";
|
|
1791
1792
|
composite: "composite";
|
|
1792
|
-
simple: "simple";
|
|
1793
1793
|
}>;
|
|
1794
1794
|
name: z.ZodString;
|
|
1795
1795
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -1953,11 +1953,11 @@ declare const PUBLIC_SCHEMAS: {
|
|
|
1953
1953
|
quantity: z.ZodNumber;
|
|
1954
1954
|
line_key: z.ZodString;
|
|
1955
1955
|
line_type: z.ZodEnum<{
|
|
1956
|
+
simple: "simple";
|
|
1956
1957
|
service: "service";
|
|
1957
1958
|
digital: "digital";
|
|
1958
1959
|
bundle: "bundle";
|
|
1959
1960
|
composite: "composite";
|
|
1960
|
-
simple: "simple";
|
|
1961
1961
|
}>;
|
|
1962
1962
|
name: z.ZodString;
|
|
1963
1963
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -2131,10 +2131,10 @@ declare const PUBLIC_SCHEMAS: {
|
|
|
2131
2131
|
requires_authorization: z.ZodBoolean;
|
|
2132
2132
|
authorization_type: z.ZodOptional<z.ZodEnum<{
|
|
2133
2133
|
phone: "phone";
|
|
2134
|
-
address: "address";
|
|
2135
2134
|
otp: "otp";
|
|
2136
2135
|
pin: "pin";
|
|
2137
2136
|
birthday: "birthday";
|
|
2137
|
+
address: "address";
|
|
2138
2138
|
}>>;
|
|
2139
2139
|
next_action: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
2140
2140
|
type: z.ZodLiteral<"none">;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cimplify/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "Cimplify Commerce SDK for storefronts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cimplify",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"libphonenumber-js": "1.12.41",
|
|
153
153
|
"react-day-picker": "^9.14.0",
|
|
154
154
|
"tailwind-merge": "^3.5.0",
|
|
155
|
+
"xss": "^1.0.15",
|
|
155
156
|
"zod": "^4.4.3"
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
"price-range",
|
|
11
11
|
"availability-badge",
|
|
12
12
|
"sale-badge",
|
|
13
|
+
"rich-text",
|
|
13
14
|
"cn"
|
|
14
15
|
],
|
|
15
16
|
"files": [
|
|
16
17
|
{
|
|
17
18
|
"path": "layouts/default-product-layout.tsx",
|
|
18
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n PropertiesTable,\n TagPills,\n InventoryBadge,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { resolveGallery } from \"../../gallery\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { PriceRange } from \"@cimplify/sdk/react\";\nimport { AvailabilityBadge } from \"@cimplify/sdk/react\";\nimport { SaleBadge } from \"@cimplify/sdk/react\";\nimport { parsePrice, isOnSale, getBasePrice, getDiscountPercentage } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function DefaultProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const [selectedVariantId, setSelectedVariantId] = React.useState<string | undefined>();\n const images = resolveGallery(product, selectedVariantId);\n const hasRange = (product.quantity_pricing && product.quantity_pricing.length > 1)\n || (product.variants && product.variants.length > 1);\n const onSale = isOnSale(product);\n const hasPhysicalDetails = product.sku || product.vendor || product.material;\n\n return (\n <div data-cimplify-product-layout=\"default\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Brand + Tags */}\n <div className=\"flex flex-wrap items-center gap-2\">\n {product.vendor && (\n <span className=\"text-xs font-semibold text-muted-foreground uppercase tracking-widest\">\n {product.vendor}\n </span>\n )}\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name */}\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n\n {/* Price */}\n <div className=\"flex items-baseline gap-3\">\n {hasRange ? (\n <PriceRange product={product} className=\"text-2xl font-extrabold\" />\n ) : onSale ? (\n <>\n <Price amount={product.default_price} className=\"text-2xl font-extrabold\" />\n <Price amount={getBasePrice(product)} className=\"text-lg text-muted-foreground line-through\" />\n <span className=\"text-sm font-bold text-destructive bg-destructive/10 px-2 py-0.5\">\n Save {getDiscountPercentage(product)}%\n </span>\n </>\n ) : (\n <Price amount={product.default_price} className=\"text-2xl font-semibold\" />\n )}\n </div>\n\n {/* Inventory */}\n <InventoryBadge product={product} />\n\n {/* Description */}\n {product.description
|
|
19
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n PropertiesTable,\n TagPills,\n InventoryBadge,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { resolveGallery } from \"../../gallery\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { PriceRange } from \"@cimplify/sdk/react\";\nimport { AvailabilityBadge } from \"@cimplify/sdk/react\";\nimport { SaleBadge } from \"@cimplify/sdk/react\";\nimport { parsePrice, isOnSale, getBasePrice, getDiscountPercentage } from \"@cimplify/sdk\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function DefaultProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const [selectedVariantId, setSelectedVariantId] = React.useState<string | undefined>();\n const images = resolveGallery(product, selectedVariantId);\n const hasRange = (product.quantity_pricing && product.quantity_pricing.length > 1)\n || (product.variants && product.variants.length > 1);\n const onSale = isOnSale(product);\n const hasPhysicalDetails = product.sku || product.vendor || product.material;\n\n return (\n <div data-cimplify-product-layout=\"default\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Brand + Tags */}\n <div className=\"flex flex-wrap items-center gap-2\">\n {product.vendor && (\n <span className=\"text-xs font-semibold text-muted-foreground uppercase tracking-widest\">\n {product.vendor}\n </span>\n )}\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name */}\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n\n {/* Price */}\n <div className=\"flex items-baseline gap-3\">\n {hasRange ? (\n <PriceRange product={product} className=\"text-2xl font-extrabold\" />\n ) : onSale ? (\n <>\n <Price amount={product.default_price} className=\"text-2xl font-extrabold\" />\n <Price amount={getBasePrice(product)} className=\"text-lg text-muted-foreground line-through\" />\n <span className=\"text-sm font-bold text-destructive bg-destructive/10 px-2 py-0.5\">\n Save {getDiscountPercentage(product)}%\n </span>\n </>\n ) : (\n <Price amount={product.default_price} className=\"text-2xl font-semibold\" />\n )}\n </div>\n\n {/* Inventory */}\n <InventoryBadge product={product} />\n\n {/* Description */}\n <RichText html={product.description} data-cimplify-product-layout-description />\n\n {/* Physical product details */}\n {hasPhysicalDetails && (\n <div data-cimplify-product-layout-details className=\"space-y-1 text-sm text-muted-foreground\">\n {product.sku && <p>SKU: <span className=\"font-mono text-foreground\">{product.sku}</span></p>}\n {product.barcode && <p>Barcode: <span className=\"font-mono text-foreground\">{product.barcode}</span></p>}\n {product.material && <p>Material: <span className=\"text-foreground\">{product.material}</span></p>}\n {product.vendor && <p>Brand: <span className=\"text-foreground\">{product.vendor}</span></p>}\n {(product.length_mm || product.width_mm || product.height_mm) && (\n <p>Dimensions: <span className=\"text-foreground\">\n {[product.length_mm, product.width_mm, product.height_mm]\n .filter(Boolean)\n .map((d) => `${(d! / 10).toFixed(1)} cm`)\n .join(\" x \")}\n </span></p>\n )}\n {product.item_condition && product.item_condition !== \"new\" && (\n <p>Condition: <span className=\"text-foreground capitalize\">{product.item_condition}</span></p>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Properties */}\n <PropertiesTable properties={product.properties || []} />\n\n {/* Customizer */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n onVariantChange={(variantId) => setSelectedVariantId(variantId)}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n />\n )}\n </div>\n );\n}\n"
|
|
19
20
|
}
|
|
20
21
|
]
|
|
21
22
|
}
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
"product-image-gallery",
|
|
8
8
|
"product-customizer",
|
|
9
9
|
"price",
|
|
10
|
+
"rich-text",
|
|
10
11
|
"cn"
|
|
11
12
|
],
|
|
12
13
|
"files": [
|
|
13
14
|
{
|
|
14
15
|
"path": "layouts/digital-product-layout.tsx",
|
|
15
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst DIGITAL_TYPE_LABELS: Record<string, string> = {\n download: \"Digital download\",\n license: \"Software license\",\n event_ticket: \"Event ticket\",\n access_pass: \"Access pass\",\n gift_card: \"Gift card\",\n};\n\nexport function DigitalProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const typeLabel = product.digital_type\n ? DIGITAL_TYPE_LABELS[product.digital_type] || product.digital_type\n : \"Digital product\";\n\n const isTicket = product.digital_type === \"event_ticket\";\n const isAccessPass = product.digital_type === \"access_pass\";\n const isDownload = product.digital_type === \"download\" || product.digital_type === \"license\";\n\n return (\n <div data-cimplify-product-layout=\"digital\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Type badge + tags */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <span data-cimplify-product-layout-badge className=\"text-xs font-semibold uppercase tracking-wider text-primary bg-primary/10 px-2.5 py-1 rounded-full\">\n {typeLabel}\n </span>\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} className=\"text-2xl font-bold mt-2 block\" />\n </div>\n\n {/* Description */}\n {product.description
|
|
16
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst DIGITAL_TYPE_LABELS: Record<string, string> = {\n download: \"Digital download\",\n license: \"Software license\",\n event_ticket: \"Event ticket\",\n access_pass: \"Access pass\",\n gift_card: \"Gift card\",\n};\n\nexport function DigitalProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const typeLabel = product.digital_type\n ? DIGITAL_TYPE_LABELS[product.digital_type] || product.digital_type\n : \"Digital product\";\n\n const isTicket = product.digital_type === \"event_ticket\";\n const isAccessPass = product.digital_type === \"access_pass\";\n const isDownload = product.digital_type === \"download\" || product.digital_type === \"license\";\n\n return (\n <div data-cimplify-product-layout=\"digital\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Type badge + tags */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <span data-cimplify-product-layout-badge className=\"text-xs font-semibold uppercase tracking-wider text-primary bg-primary/10 px-2.5 py-1 rounded-full\">\n {typeLabel}\n </span>\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} className=\"text-2xl font-bold mt-2 block\" />\n </div>\n\n {/* Description */}\n <RichText html={product.description} data-cimplify-product-layout-description />\n\n {/* Delivery notice */}\n {isDownload && (\n <div data-cimplify-product-layout-delivery className=\"flex items-center gap-3 px-4 py-3 bg-muted rounded-lg\">\n <svg className=\"w-5 h-5 text-primary shrink-0\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n <div>\n <p className=\"text-sm font-medium\">Instant delivery</p>\n <p className=\"text-xs text-muted-foreground\">Download immediately after purchase</p>\n </div>\n </div>\n )}\n\n {/* Event info */}\n {isTicket && (product.event_date || product.venue) && (\n <div data-cimplify-product-layout-event className=\"border border-border p-4 space-y-2\">\n {product.event_date && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Date:</span>\n <span className=\"font-medium\">{new Date(product.event_date).toLocaleDateString(undefined, { weekday: \"long\", year: \"numeric\", month: \"long\", day: \"numeric\" })}</span>\n </div>\n )}\n {product.venue && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Venue:</span>\n <span className=\"font-medium\">{product.venue}</span>\n </div>\n )}\n {product.ticket_type && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Type:</span>\n <span className=\"font-medium capitalize\">{product.ticket_type}</span>\n </div>\n )}\n </div>\n )}\n\n {/* Access pass info */}\n {isAccessPass && (product.access_type || product.access_duration_days) && (\n <div data-cimplify-product-layout-access className=\"border border-border p-4 space-y-2\">\n {product.access_type && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Access:</span>\n <span className=\"font-medium capitalize\">{product.access_type}</span>\n </div>\n )}\n {product.access_level && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Level:</span>\n <span className=\"font-medium capitalize\">{product.access_level}</span>\n </div>\n )}\n {product.access_duration_days && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Duration:</span>\n <span className=\"font-medium\">{product.access_duration_days} days</span>\n </div>\n )}\n </div>\n )}\n\n {/* File details */}\n {isDownload && (product.file_size_mb || product.file_type || product.version || product.max_downloads) && (\n <div data-cimplify-product-layout-file-details className=\"grid grid-cols-2 gap-3\">\n {product.file_type && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Format</p>\n <p className=\"text-sm font-semibold\">{product.file_type.toUpperCase()}</p>\n </div>\n )}\n {product.file_size_mb != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Size</p>\n <p className=\"text-sm font-semibold\">{product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)} GB` : `${product.file_size_mb} MB`}</p>\n </div>\n )}\n {product.max_downloads != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Downloads</p>\n <p className=\"text-sm font-semibold\">{product.max_downloads}</p>\n </div>\n )}\n {product.version && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Version</p>\n <p className=\"text-sm font-semibold\">v{product.version}</p>\n </div>\n )}\n {product.download_expires_days != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Expires</p>\n <p className=\"text-sm font-semibold\">{product.download_expires_days} days</p>\n </div>\n )}\n {product.max_activations != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Activations</p>\n <p className=\"text-sm font-semibold\">{product.max_activations}</p>\n </div>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (license variants, billing plans, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n showSpecialInstructions={false}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title=\"More digital products\"\n />\n )}\n </div>\n );\n}\n"
|
|
16
17
|
}
|
|
17
18
|
]
|
|
18
19
|
}
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
"product-image-gallery",
|
|
8
8
|
"product-customizer",
|
|
9
9
|
"price",
|
|
10
|
+
"rich-text",
|
|
10
11
|
"cn"
|
|
11
12
|
],
|
|
12
13
|
"files": [
|
|
13
14
|
{
|
|
14
15
|
"path": "layouts/food-product-layout.tsx",
|
|
15
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n MetadataStringList,\n CustomAttributesTable,\n TagPills,\n getMetadataStringList,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\n\nexport function FoodProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const tags = product.tags || [];\n const ingredients = getMetadataStringList(product.metadata, \"ingredients\");\n const pairings = getMetadataStringList(product.metadata, \"pairings\");\n\n return (\n <div data-cimplify-product-layout=\"food\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Tags + Allergens */}\n <div className=\"flex flex-wrap gap-1.5\">\n {tags.map((tag) => (\n <span key={tag} className=\"inline-flex items-center gap-1 px-2.5 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full\">\n {tag}\n </span>\n ))}\n {product.allergies && product.allergies.map((allergy) => (\n <span key={allergy} className=\"inline-flex items-center gap-1 px-2.5 py-1 bg-amber-100 text-amber-800 text-xs font-medium rounded-full\">\n Contains {allergy}\n </span>\n ))}\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-bold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} className=\"text-2xl font-semibold mt-2 block\" />\n </div>\n\n {/* Quick stats */}\n {(product.calories != null || product.preparation_time_minutes != null) && (\n <div data-cimplify-product-layout-stats className=\"flex flex-wrap gap-4 py-3 border-y border-border\">\n {product.preparation_time_minutes != null && (\n <span className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n <strong className=\"text-foreground\">{product.preparation_time_minutes} min</strong> prep time\n </span>\n )}\n {product.calories != null && (\n <span className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n <strong className=\"text-foreground\">{product.calories}</strong> calories\n </span>\n )}\n </div>\n )}\n\n {/* Description */}\n {product.description
|
|
16
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n MetadataStringList,\n CustomAttributesTable,\n TagPills,\n getMetadataStringList,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\n\nexport function FoodProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const tags = product.tags || [];\n const ingredients = getMetadataStringList(product.metadata, \"ingredients\");\n const pairings = getMetadataStringList(product.metadata, \"pairings\");\n\n return (\n <div data-cimplify-product-layout=\"food\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Tags + Allergens */}\n <div className=\"flex flex-wrap gap-1.5\">\n {tags.map((tag) => (\n <span key={tag} className=\"inline-flex items-center gap-1 px-2.5 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full\">\n {tag}\n </span>\n ))}\n {product.allergies && product.allergies.map((allergy) => (\n <span key={allergy} className=\"inline-flex items-center gap-1 px-2.5 py-1 bg-amber-100 text-amber-800 text-xs font-medium rounded-full\">\n Contains {allergy}\n </span>\n ))}\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-bold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} className=\"text-2xl font-semibold mt-2 block\" />\n </div>\n\n {/* Quick stats */}\n {(product.calories != null || product.preparation_time_minutes != null) && (\n <div data-cimplify-product-layout-stats className=\"flex flex-wrap gap-4 py-3 border-y border-border\">\n {product.preparation_time_minutes != null && (\n <span className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n <strong className=\"text-foreground\">{product.preparation_time_minutes} min</strong> prep time\n </span>\n )}\n {product.calories != null && (\n <span className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n <strong className=\"text-foreground\">{product.calories}</strong> calories\n </span>\n )}\n </div>\n )}\n\n {/* Description */}\n <RichText html={product.description} data-cimplify-product-layout-description />\n\n {/* Ingredients + Pairings */}\n {(ingredients.length > 0 || pairings.length > 0) && (\n <div className=\"grid sm:grid-cols-2 gap-4\">\n <MetadataStringList items={ingredients} label=\"Key Ingredients\" />\n <MetadataStringList items={pairings} label=\"Pairings\" />\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (variants, add-ons, composites, bundles, billing, volume) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title=\"You may also enjoy\"\n />\n )}\n </div>\n );\n}\n"
|
|
16
17
|
}
|
|
17
18
|
]
|
|
18
19
|
}
|
package/registry/index.json
CHANGED
|
@@ -124,6 +124,15 @@
|
|
|
124
124
|
"cn"
|
|
125
125
|
]
|
|
126
126
|
},
|
|
127
|
+
{
|
|
128
|
+
"name": "rich-text",
|
|
129
|
+
"title": "RichText",
|
|
130
|
+
"description": "Renders sanitized merchant-authored HTML (descriptions) with theme-aware prose styling. Edge-safe XSS sanitization via the pure-JS xss library.",
|
|
131
|
+
"type": "component",
|
|
132
|
+
"registryDependencies": [
|
|
133
|
+
"cn"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
127
136
|
{
|
|
128
137
|
"name": "product-sheet",
|
|
129
138
|
"title": "ProductSheet",
|
|
@@ -133,6 +142,7 @@
|
|
|
133
142
|
"price",
|
|
134
143
|
"product-image-gallery",
|
|
135
144
|
"product-customizer",
|
|
145
|
+
"rich-text",
|
|
136
146
|
"cn"
|
|
137
147
|
]
|
|
138
148
|
},
|
|
@@ -555,6 +565,7 @@
|
|
|
555
565
|
"price-range",
|
|
556
566
|
"availability-badge",
|
|
557
567
|
"sale-badge",
|
|
568
|
+
"rich-text",
|
|
558
569
|
"cn"
|
|
559
570
|
]
|
|
560
571
|
},
|
|
@@ -567,6 +578,7 @@
|
|
|
567
578
|
"product-image-gallery",
|
|
568
579
|
"product-customizer",
|
|
569
580
|
"price",
|
|
581
|
+
"rich-text",
|
|
570
582
|
"cn"
|
|
571
583
|
]
|
|
572
584
|
},
|
|
@@ -581,6 +593,7 @@
|
|
|
581
593
|
"price",
|
|
582
594
|
"price-range",
|
|
583
595
|
"volume-pricing",
|
|
596
|
+
"rich-text",
|
|
584
597
|
"cn"
|
|
585
598
|
]
|
|
586
599
|
},
|
|
@@ -593,6 +606,7 @@
|
|
|
593
606
|
"product-image-gallery",
|
|
594
607
|
"product-customizer",
|
|
595
608
|
"price",
|
|
609
|
+
"rich-text",
|
|
596
610
|
"cn"
|
|
597
611
|
]
|
|
598
612
|
},
|
|
@@ -605,6 +619,7 @@
|
|
|
605
619
|
"product-image-gallery",
|
|
606
620
|
"product-customizer",
|
|
607
621
|
"price",
|
|
622
|
+
"rich-text",
|
|
608
623
|
"cn"
|
|
609
624
|
]
|
|
610
625
|
},
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
"price",
|
|
8
8
|
"product-image-gallery",
|
|
9
9
|
"product-customizer",
|
|
10
|
+
"rich-text",
|
|
10
11
|
"cn"
|
|
11
12
|
],
|
|
12
13
|
"files": [
|
|
13
14
|
{
|
|
14
15
|
"path": "product-sheet.tsx",
|
|
15
|
-
"content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport type { Product, ProductWithDetails, VariantView } from \"@cimplify/sdk\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductSheetClassNames {\n root?: string;\n image?: string;\n header?: string;\n name?: string;\n price?: string;\n description?: string;\n customizer?: string;\n loading?: string;\n}\n\nexport interface ProductSheetProps {\n /** A slim Product (triggers lazy-fetch) or a fully-loaded ProductWithDetails. */\n product: Product | ProductWithDetails;\n /** Called when the sheet should close. */\n onClose?: () => void;\n /** Override the default 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: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n className?: string;\n classNames?: ProductSheetClassNames;\n}\n\nfunction isProductWithDetails(\n product: Product | ProductWithDetails,\n): product is ProductWithDetails {\n return \"variants\" in product;\n}\n\n/**\n * ProductSheet — full product detail view composing gallery, header, and customizer.\n *\n * When given a slim `Product`, it lazy-fetches the full details via `useProduct`.\n * When given a `ProductWithDetails`, it skips the fetch entirely.\n */\nexport function ProductSheet({\n product,\n onClose,\n onAddToCart,\n renderImage,\n className,\n classNames,\n}: ProductSheetProps): React.ReactElement {\n const needsFetch = !isProductWithDetails(product);\n const { product: fetched, isLoading } = useProduct(\n product.slug ?? product.id,\n { enabled: needsFetch },\n );\n const fullProduct = needsFetch ? fetched : (product as ProductWithDetails);\n\n // Loading state\n if (isLoading && !fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div\n data-cimplify-product-sheet-skeleton\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"1rem\",\n }}\n >\n <div\n style={{\n aspectRatio: \"4/3\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.5rem\",\n }}\n />\n <div\n style={{\n height: \"1.5rem\",\n width: \"60%\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.25rem\",\n }}\n />\n <div\n style={{\n height: \"1rem\",\n width: \"30%\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.25rem\",\n }}\n />\n </div>\n </div>\n );\n }\n\n // Error state\n if (!fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n >\n <p>Product not found.</p>\n </div>\n );\n }\n\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>(\n undefined,\n );\n\n const variantImages = selectedVariant?.images?.filter(Boolean) ?? [];\n const productImages: string[] = [];\n if (fullProduct.images && fullProduct.images.length > 0) {\n productImages.push(...fullProduct.images.filter(Boolean));\n } else if (fullProduct.image_url) {\n productImages.push(fullProduct.image_url);\n }\n const images: string[] =\n variantImages.length > 0 ? variantImages : productImages;\n\n const hasMultipleImages = images.length > 1;\n const singleImage = images[0];\n\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"1rem\" }}\n >\n {/* Image area */}\n {hasMultipleImages ? (\n <ProductImageGallery\n images={images}\n productName={fullProduct.name}\n className={classNames?.image}\n />\n ) : singleImage ? (\n <div data-cimplify-product-sheet-image className={classNames?.image}>\n {renderImage ? (\n renderImage({ src: singleImage, alt: fullProduct.name })\n ) : (\n <img\n src={singleImage}\n alt={fullProduct.name}\n style={{ width: \"100%\", height: \"auto\", objectFit: \"cover\" }}\n />\n )}\n </div>\n ) : null}\n\n {/* Header */}\n <div data-cimplify-product-sheet-header className={classNames?.header}>\n <h2\n data-cimplify-product-sheet-name\n className={classNames?.name}\n style={{ margin: 0 }}\n >\n {fullProduct.name}\n </h2>\n <Price\n amount={fullProduct.default_price}\n className={classNames?.price}\n />\n </div>\n\n {/* Description — merchant-authored HTML from the catalogue. */}\n {fullProduct.description
|
|
16
|
+
"content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport type { Product, ProductWithDetails, VariantView } from \"@cimplify/sdk\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductSheetClassNames {\n root?: string;\n image?: string;\n header?: string;\n name?: string;\n price?: string;\n description?: string;\n customizer?: string;\n loading?: string;\n}\n\nexport interface ProductSheetProps {\n /** A slim Product (triggers lazy-fetch) or a fully-loaded ProductWithDetails. */\n product: Product | ProductWithDetails;\n /** Called when the sheet should close. */\n onClose?: () => void;\n /** Override the default 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: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n className?: string;\n classNames?: ProductSheetClassNames;\n}\n\nfunction isProductWithDetails(\n product: Product | ProductWithDetails,\n): product is ProductWithDetails {\n return \"variants\" in product;\n}\n\n/**\n * ProductSheet — full product detail view composing gallery, header, and customizer.\n *\n * When given a slim `Product`, it lazy-fetches the full details via `useProduct`.\n * When given a `ProductWithDetails`, it skips the fetch entirely.\n */\nexport function ProductSheet({\n product,\n onClose,\n onAddToCart,\n renderImage,\n className,\n classNames,\n}: ProductSheetProps): React.ReactElement {\n const needsFetch = !isProductWithDetails(product);\n const { product: fetched, isLoading } = useProduct(\n product.slug ?? product.id,\n { enabled: needsFetch },\n );\n const fullProduct = needsFetch ? fetched : (product as ProductWithDetails);\n\n // Loading state\n if (isLoading && !fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div\n data-cimplify-product-sheet-skeleton\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"1rem\",\n }}\n >\n <div\n style={{\n aspectRatio: \"4/3\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.5rem\",\n }}\n />\n <div\n style={{\n height: \"1.5rem\",\n width: \"60%\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.25rem\",\n }}\n />\n <div\n style={{\n height: \"1rem\",\n width: \"30%\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.25rem\",\n }}\n />\n </div>\n </div>\n );\n }\n\n // Error state\n if (!fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n >\n <p>Product not found.</p>\n </div>\n );\n }\n\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>(\n undefined,\n );\n\n const variantImages = selectedVariant?.images?.filter(Boolean) ?? [];\n const productImages: string[] = [];\n if (fullProduct.images && fullProduct.images.length > 0) {\n productImages.push(...fullProduct.images.filter(Boolean));\n } else if (fullProduct.image_url) {\n productImages.push(fullProduct.image_url);\n }\n const images: string[] =\n variantImages.length > 0 ? variantImages : productImages;\n\n const hasMultipleImages = images.length > 1;\n const singleImage = images[0];\n\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"1rem\" }}\n >\n {/* Image area */}\n {hasMultipleImages ? (\n <ProductImageGallery\n images={images}\n productName={fullProduct.name}\n className={classNames?.image}\n />\n ) : singleImage ? (\n <div data-cimplify-product-sheet-image className={classNames?.image}>\n {renderImage ? (\n renderImage({ src: singleImage, alt: fullProduct.name })\n ) : (\n <img\n src={singleImage}\n alt={fullProduct.name}\n style={{ width: \"100%\", height: \"auto\", objectFit: \"cover\" }}\n />\n )}\n </div>\n ) : null}\n\n {/* Header */}\n <div data-cimplify-product-sheet-header className={classNames?.header}>\n <h2\n data-cimplify-product-sheet-name\n className={classNames?.name}\n style={{ margin: 0 }}\n >\n {fullProduct.name}\n </h2>\n <Price\n amount={fullProduct.default_price}\n className={classNames?.price}\n />\n </div>\n\n {/* Description — merchant-authored HTML from the catalogue. */}\n <RichText\n html={fullProduct.description}\n data-cimplify-product-sheet-description\n className={cn(\"text-sm\", classNames?.description)}\n />\n\n {/* Customizer */}\n <ProductCustomizer\n product={fullProduct}\n onAddToCart={onAddToCart}\n onVariantChange={(_id, variant) => setSelectedVariant(variant)}\n className={classNames?.customizer}\n />\n </div>\n );\n}\n"
|
|
16
17
|
}
|
|
17
18
|
]
|
|
18
19
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rich-text",
|
|
3
|
+
"title": "RichText",
|
|
4
|
+
"description": "Renders sanitized merchant-authored HTML (descriptions) with theme-aware prose styling. Edge-safe XSS sanitization via the pure-JS xss library.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"cn"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "rich-text.tsx",
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useMemo } from \"react\";\nimport { FilterXSS, safeAttrValue as baseSafeAttrValue } from \"xss\";\nimport { cn } from \"@cimplify/sdk/react\";\n\n// Hosts allowed for <iframe> embeds. Everything else has its src blanked.\nconst ALLOWED_EMBED_HOSTS = new Set<string>([\n \"www.youtube.com\",\n \"youtube.com\",\n \"www.youtube-nocookie.com\",\n \"youtube-nocookie.com\",\n \"player.vimeo.com\",\n]);\n\n// Inline CSS properties we permit (via the xss css filter). Anything else —\n// position, z-index, behaviours, url() exfil vectors — is dropped.\nconst ALLOWED_CSS_PROPERTIES: Record<string, boolean> = {\n \"text-align\": true,\n color: true,\n \"background-color\": true,\n \"font-weight\": true,\n \"font-style\": true,\n \"text-decoration\": true,\n \"vertical-align\": true,\n width: true,\n height: true,\n};\n\nconst STYLEABLE = [\"style\", \"class\"];\n\n/**\n * Allowlist covering Amazon/Shopify-grade product copy: rich text, spec and\n * comparison tables, figures, definition lists, collapsible sections, and\n * host-locked video embeds. Tags outside this set are dropped; `javascript:`\n * URLs, event handlers, and unlisted CSS properties are stripped.\n */\nconst RICH_TEXT_WHITELIST: Record<string, string[]> = {\n p: STYLEABLE,\n br: [],\n div: STYLEABLE,\n section: [\"class\"],\n span: STYLEABLE,\n strong: [],\n b: [],\n em: [],\n i: [],\n u: [],\n s: [],\n strike: [],\n del: [],\n ins: [],\n mark: [],\n small: [],\n sub: [],\n sup: [],\n h1: STYLEABLE,\n h2: STYLEABLE,\n h3: STYLEABLE,\n h4: STYLEABLE,\n h5: STYLEABLE,\n h6: STYLEABLE,\n ul: [\"class\"],\n ol: [\"start\", \"type\", \"class\"],\n li: [\"class\"],\n dl: [\"class\"],\n dt: [\"class\"],\n dd: [\"class\"],\n blockquote: [\"class\"],\n code: [],\n pre: [\"class\"],\n hr: [],\n a: [\"href\", \"title\", \"target\", \"rel\"],\n img: [\"src\", \"alt\", \"title\", \"width\", \"height\", \"loading\", \"class\", \"style\"],\n figure: [\"class\"],\n figcaption: [\"class\"],\n details: [],\n summary: [],\n table: [\"class\", \"style\"],\n thead: [],\n tbody: [],\n tfoot: [],\n caption: [],\n colgroup: [],\n col: [\"span\", \"style\"],\n tr: [\"class\"],\n th: [\"colspan\", \"rowspan\", \"scope\", \"align\", \"class\", \"style\"],\n td: [\"colspan\", \"rowspan\", \"align\", \"class\", \"style\"],\n iframe: [\"src\", \"width\", \"height\", \"allow\", \"allowfullscreen\", \"frameborder\", \"title\", \"loading\"],\n};\n\nfunction safeAttrValue(\n tag: string,\n name: string,\n value: string,\n cssFilter: Parameters<typeof baseSafeAttrValue>[3],\n): string {\n if (tag === \"iframe\" && name === \"src\") {\n try {\n const url = new URL(value);\n if (url.protocol === \"https:\" && ALLOWED_EMBED_HOSTS.has(url.hostname)) {\n return value;\n }\n } catch {\n // malformed URL — fall through to blank\n }\n return \"\";\n }\n return baseSafeAttrValue(tag, name, value, cssFilter);\n}\n\n// A single configured filter instance, reused across every call.\nconst FILTER = new FilterXSS({\n whiteList: RICH_TEXT_WHITELIST,\n css: { whiteList: ALLOWED_CSS_PROPERTIES },\n safeAttrValue,\n stripIgnoreTag: true,\n stripIgnoreTagBody: [\"script\", \"style\"],\n allowCommentTag: false,\n});\n\n/**\n * Sanitize merchant-authored HTML for safe rendering. Runs identically on the\n * Cloudflare Workers edge (SSR) and in the browser — no DOM dependency.\n */\nexport function sanitizeRichTextHtml(html: string): string {\n return FILTER.process(html);\n}\n\n// Theme-aware prose styling. Wired to the consumer's own design tokens\n// (`foreground`, `muted-foreground`, `primary`, `border`, `muted`) so rich\n// text inherits each storefront's type scale and colours.\nconst RICH_TEXT_PROSE = cn(\n \"text-muted-foreground leading-relaxed\",\n \"[&>*:first-child]:mt-0 [&>*:last-child]:mb-0\",\n \"[&_p]:my-3\",\n \"[&_h1]:mt-6 [&_h1]:mb-2 [&_h1]:text-xl [&_h1]:font-semibold [&_h1]:leading-snug [&_h1]:text-foreground\",\n \"[&_h2]:mt-5 [&_h2]:mb-2 [&_h2]:text-lg [&_h2]:font-semibold [&_h2]:leading-snug [&_h2]:text-foreground\",\n \"[&_h3]:mt-4 [&_h3]:mb-1.5 [&_h3]:text-base [&_h3]:font-semibold [&_h3]:text-foreground\",\n \"[&_h4]:mt-4 [&_h4]:mb-1.5 [&_h4]:text-sm [&_h4]:font-semibold [&_h4]:text-foreground\",\n \"[&_h5]:mt-3 [&_h5]:mb-1 [&_h5]:font-semibold [&_h5]:text-foreground\",\n \"[&_h6]:mt-3 [&_h6]:mb-1 [&_h6]:font-semibold [&_h6]:text-foreground\",\n \"[&_strong]:font-semibold [&_strong]:text-foreground [&_b]:font-semibold [&_b]:text-foreground\",\n \"[&_em]:italic [&_i]:italic\",\n \"[&_u]:underline [&_ins]:underline [&_s]:line-through [&_del]:line-through\",\n \"[&_mark]:rounded [&_mark]:px-0.5 [&_sub]:align-sub [&_sub]:text-[0.75em] [&_sup]:align-super [&_sup]:text-[0.75em]\",\n \"[&_a]:font-medium [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2\",\n \"[&_ul]:my-3 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:space-y-1\",\n \"[&_ol]:my-3 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:space-y-1\",\n \"[&_li]:pl-1 [&_li>p]:my-0 [&_ul_ul]:my-1 [&_ol_ol]:my-1 [&_ul_ul]:list-[circle]\",\n \"[&_dl]:my-3 [&_dt]:font-semibold [&_dt]:text-foreground [&_dd]:mb-2 [&_dd]:pl-4\",\n \"[&_blockquote]:my-3 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:italic\",\n \"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.85em]\",\n \"[&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-[0.85em]\",\n \"[&_pre>code]:bg-transparent [&_pre>code]:p-0\",\n \"[&_hr]:my-4 [&_hr]:border-border\",\n \"[&_figure]:my-4 [&_figcaption]:mt-2 [&_figcaption]:text-center [&_figcaption]:text-xs [&_figcaption]:text-muted-foreground\",\n \"[&_img]:my-3 [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md\",\n \"[&_details]:my-3 [&_details]:rounded-md [&_details]:border [&_details]:border-border [&_details]:p-3\",\n \"[&_summary]:cursor-pointer [&_summary]:font-medium [&_summary]:text-foreground\",\n \"[&_table]:my-4 [&_table]:w-full [&_table]:border-collapse [&_table]:text-sm\",\n \"[&_caption]:caption-bottom [&_caption]:mt-2 [&_caption]:text-xs [&_caption]:text-muted-foreground\",\n \"[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:font-semibold [&_th]:text-foreground\",\n \"[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2 [&_td]:align-top\",\n \"[&_iframe]:my-4 [&_iframe]:aspect-video [&_iframe]:w-full [&_iframe]:rounded-md\",\n);\n\nexport interface RichTextProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"dangerouslySetInnerHTML\" | \"children\"> {\n /** Merchant-authored HTML (e.g. a product description). Sanitized before render. */\n html?: string | null;\n}\n\n/**\n * RichText — renders sanitized merchant-authored HTML with theme-aware prose\n * styling. Headings, lists, tables, figures, quotes, code, collapsible\n * sections and host-locked embeds all display correctly; scripts, event\n * handlers and unlisted CSS are stripped.\n *\n * Returns `null` when there's no renderable content.\n */\nexport function RichText({ html, className, ...rest }: RichTextProps): React.ReactElement | null {\n const clean = useMemo(() => (html ? sanitizeRichTextHtml(html) : \"\"), [html]);\n\n if (!clean) {\n return null;\n }\n\n return (\n <div\n data-cimplify-rich-text\n className={cn(RICH_TEXT_PROSE, className)}\n dangerouslySetInnerHTML={{ __html: clean }}\n {...rest}\n />\n );\n}\n"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
"product-image-gallery",
|
|
8
8
|
"product-customizer",
|
|
9
9
|
"price",
|
|
10
|
+
"rich-text",
|
|
10
11
|
"cn"
|
|
11
12
|
],
|
|
12
13
|
"files": [
|
|
13
14
|
{
|
|
14
15
|
"path": "layouts/service-product-layout.tsx",
|
|
15
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { formatDuration } from \"../utils/format-duration\";\n\nexport function ServiceProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n const depositAmount = hasDeposit ? parsePrice(product.deposit_amount!) : 0;\n const cancellationHours = product.cancellation_window_minutes\n ? Math.floor(product.cancellation_window_minutes / 60)\n : null;\n\n return (\n <div data-cimplify-product-layout=\"service\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Tags */}\n <TagPills tags={product.tags || []} />\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} className=\"text-2xl font-bold text-primary mt-2 block\" />\n </div>\n\n {/* Service info pills */}\n <div data-cimplify-product-layout-service-info className=\"flex flex-wrap gap-2\">\n {product.duration_minutes != null && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n {hasDeposit && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n <Price amount={depositAmount} /> deposit\n </span>\n )}\n {product.general_service_capacity != null && product.general_service_capacity > 1 && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n Up to {product.general_service_capacity} people\n </span>\n )}\n {product.buffer_before_minutes != null && product.buffer_before_minutes > 0 && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n {product.buffer_before_minutes} min buffer\n </span>\n )}\n </div>\n\n {/* Description */}\n {product.description
|
|
16
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { formatDuration } from \"../utils/format-duration\";\n\nexport function ServiceProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n const depositAmount = hasDeposit ? parsePrice(product.deposit_amount!) : 0;\n const cancellationHours = product.cancellation_window_minutes\n ? Math.floor(product.cancellation_window_minutes / 60)\n : null;\n\n return (\n <div data-cimplify-product-layout=\"service\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Tags */}\n <TagPills tags={product.tags || []} />\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} className=\"text-2xl font-bold text-primary mt-2 block\" />\n </div>\n\n {/* Service info pills */}\n <div data-cimplify-product-layout-service-info className=\"flex flex-wrap gap-2\">\n {product.duration_minutes != null && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n {hasDeposit && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n <Price amount={depositAmount} /> deposit\n </span>\n )}\n {product.general_service_capacity != null && product.general_service_capacity > 1 && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n Up to {product.general_service_capacity} people\n </span>\n )}\n {product.buffer_before_minutes != null && product.buffer_before_minutes > 0 && (\n <span className=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full\">\n {product.buffer_before_minutes} min buffer\n </span>\n )}\n </div>\n\n {/* Description */}\n <RichText html={product.description} data-cimplify-product-layout-description />\n\n {/* Staff & Resource requirements */}\n {(product.requires_specific_staff || product.requires_specific_resource) && (\n <div data-cimplify-product-layout-requirements className=\"text-sm text-muted-foreground space-y-1\">\n {product.requires_specific_staff && (\n <p>Staff selection required</p>\n )}\n {product.requires_specific_resource && (\n <p>Resource selection required</p>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (variants, scheduling, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n showSpecialInstructions={false}\n />\n\n {/* Cancellation policy */}\n {(cancellationHours != null || product.no_show_fee) && (\n <div data-cimplify-product-layout-cancellation className=\"border border-border p-4 space-y-2 text-sm\">\n <h3 className=\"font-semibold\">Cancellation Policy</h3>\n {cancellationHours != null && (\n <p className=\"text-muted-foreground\">\n Free cancellation up to <strong className=\"text-foreground\">{cancellationHours} hours</strong> before the booking\n </p>\n )}\n {product.no_show_fee && (\n <p className=\"text-muted-foreground\">\n No-show fee: <Price amount={product.no_show_fee} className=\"font-medium text-foreground\" />\n </p>\n )}\n {hasDeposit && (\n <p className=\"text-muted-foreground\">\n {product.deposit_type === \"fixed\"\n ? <><Price amount={depositAmount} className=\"font-medium text-foreground\" /> deposit charged at booking</>\n : <>{parsePrice(product.deposit_amount!)}% deposit charged at booking</>\n }\n </p>\n )}\n </div>\n )}\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title=\"Other services\"\n />\n )}\n </div>\n );\n}\n"
|
|
16
17
|
}
|
|
17
18
|
]
|
|
18
19
|
}
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"price",
|
|
10
10
|
"price-range",
|
|
11
11
|
"volume-pricing",
|
|
12
|
+
"rich-text",
|
|
12
13
|
"cn"
|
|
13
14
|
],
|
|
14
15
|
"files": [
|
|
15
16
|
{
|
|
16
17
|
"path": "layouts/wholesale-product-layout.tsx",
|
|
17
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n PropertiesTable,\n TagPills,\n InventoryBadge,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { PriceRange } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function WholesaleProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n\n return (\n <div data-cimplify-product-layout=\"wholesale\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Tags */}\n <TagPills tags={product.tags || []} />\n\n {/* SKU + Name */}\n <div>\n {product.sku && (\n <p className=\"text-xs text-muted-foreground font-medium uppercase tracking-wider mb-1\">\n SKU: {product.sku}\n </p>\n )}\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n </div>\n\n {/* Price range */}\n <div>\n {hasTiers ? (\n <PriceRange product={product} className=\"text-2xl font-extrabold\" />\n ) : (\n <Price amount={product.default_price} className=\"text-2xl font-semibold\" />\n )}\n </div>\n\n {/* Inventory */}\n <InventoryBadge product={product} />\n\n {/* Min order notice */}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <div data-cimplify-product-layout-moq className=\"flex items-center gap-3 p-3 bg-amber-50 border border-amber-200 text-sm text-amber-800\">\n Minimum order quantity: <strong>{product.min_order_quantity} units</strong>\n </div>\n )}\n\n {/* Description */}\n {product.description
|
|
18
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n PropertiesTable,\n TagPills,\n InventoryBadge,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { PriceRange } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function WholesaleProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n\n return (\n <div data-cimplify-product-layout=\"wholesale\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Tags */}\n <TagPills tags={product.tags || []} />\n\n {/* SKU + Name */}\n <div>\n {product.sku && (\n <p className=\"text-xs text-muted-foreground font-medium uppercase tracking-wider mb-1\">\n SKU: {product.sku}\n </p>\n )}\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n </div>\n\n {/* Price range */}\n <div>\n {hasTiers ? (\n <PriceRange product={product} className=\"text-2xl font-extrabold\" />\n ) : (\n <Price amount={product.default_price} className=\"text-2xl font-semibold\" />\n )}\n </div>\n\n {/* Inventory */}\n <InventoryBadge product={product} />\n\n {/* Min order notice */}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <div data-cimplify-product-layout-moq className=\"flex items-center gap-3 p-3 bg-amber-50 border border-amber-200 text-sm text-amber-800\">\n Minimum order quantity: <strong>{product.min_order_quantity} units</strong>\n </div>\n )}\n\n {/* Description */}\n <RichText html={product.description} data-cimplify-product-layout-description />\n\n {/* Volume pricing — open by default for wholesale */}\n {hasTiers && (\n <VolumePricing\n tiers={product.quantity_pricing!}\n defaultOpen\n />\n )}\n\n {/* Product details */}\n {(product.sku || product.barcode || product.vendor) && (\n <div data-cimplify-product-layout-details className=\"space-y-1 text-sm text-muted-foreground\">\n {product.barcode && <p>Barcode: <span className=\"font-mono text-foreground\">{product.barcode}</span></p>}\n {product.vendor && <p>Supplier: <span className=\"text-foreground\">{product.vendor}</span></p>}\n {product.hs_code && <p>HS Code: <span className=\"font-mono text-foreground\">{product.hs_code}</span></p>}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Properties */}\n <PropertiesTable properties={product.properties || []} />\n\n {/* Customizer (variants, billing plans, quantity, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title=\"Related products\"\n />\n )}\n </div>\n );\n}\n"
|
|
18
19
|
}
|
|
19
20
|
]
|
|
20
21
|
}
|