@cimplify/sdk 0.70.3 → 0.72.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/{chunk-UUW7WBCG.mjs → chunk-BYWJSPT4.mjs} +1 -1
- package/dist/{chunk-QXMO2LGU.mjs → chunk-J7L5LHGK.mjs} +3 -1
- package/dist/{chunk-XQ6LEDEO.js → chunk-OGQFDCZM.js} +3 -1
- package/dist/{chunk-XGKMSLMJ.js → chunk-Y64AIRGW.js} +2 -2
- package/dist/mock/cli.mjs +3 -1
- package/dist/mock/library.js +3 -1
- package/dist/mock/library.mjs +3 -1
- package/dist/mock/msw.js +3 -1
- package/dist/mock/msw.mjs +3 -1
- package/dist/react.d.mts +55 -17
- package/dist/react.d.ts +55 -17
- package/dist/react.js +148 -84
- package/dist/react.mjs +149 -85
- package/dist/server/tanstack.d.mts +74 -0
- package/dist/server/tanstack.d.ts +74 -0
- package/dist/server/tanstack.js +291 -0
- package/dist/server/tanstack.mjs +279 -0
- package/dist/testing/msw.js +2 -2
- package/dist/testing/msw.mjs +1 -1
- package/dist/testing/suite.js +21 -21
- package/dist/testing/suite.mjs +2 -2
- package/dist/testing.js +77 -77
- package/dist/testing.mjs +3 -3
- package/package.json +7 -1
- package/registry/accommodation-card.json +1 -1
- package/registry/add-on-selector.json +1 -1
- package/registry/billing-plan-selector.json +1 -1
- package/registry/booking-page.json +1 -1
- package/registry/bundle-selector.json +1 -1
- package/registry/compact-service-card.json +1 -1
- package/registry/composite-selector.json +1 -1
- package/registry/customer-input-fields.json +1 -1
- package/registry/date-slot-picker.json +1 -1
- package/registry/default-product-layout.json +1 -1
- package/registry/digital-product-card.json +1 -1
- package/registry/digital-product-layout.json +1 -1
- package/registry/food-product-card.json +1 -1
- package/registry/food-product-layout.json +1 -1
- package/registry/lease-service-card.json +1 -1
- package/registry/price-range.json +1 -1
- package/registry/price.json +1 -1
- package/registry/product-card.json +1 -1
- package/registry/product-customizer.json +1 -1
- package/registry/product-sheet.json +1 -1
- package/registry/rental-service-card.json +1 -1
- package/registry/retail-product-card.json +1 -1
- package/registry/sale-badge.json +1 -1
- package/registry/schedule-service-card.json +1 -1
- package/registry/service-product-layout.json +1 -1
- package/registry/slot-picker.json +1 -1
- package/registry/standard-service-card.json +1 -1
- package/registry/subscription-card.json +1 -1
- package/registry/variant-selector.json +1 -1
- package/registry/volume-pricing.json +1 -1
- package/registry/wholesale-product-card.json +1 -1
- package/registry/wholesale-product-layout.json +1 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "cards/standard-service-card.tsx",
|
|
14
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport { stripHtmlToText } from \"@cimplify/sdk/react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ServiceCardLayoutProps extends CardLayoutProps {\n slots?: AvailableSlot[];\n onBook?: (product: Product) => void;\n}\n\nfunction formatDuration(minutes: number, unit?: string): string {\n if (unit && unit !== \"minutes\") return `${minutes} ${unit}`;\n if (minutes >= 60) {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return m > 0 ? `${h}h ${m}m` : `${h} hr${h > 1 ? \"s\" : \"\"}`;\n }\n return `${minutes} min`;\n}\n\nexport function StandardServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Duration pill */}\n {product.duration_minutes != null && (\n <span className=\"absolute bottom-3 left-3 inline-flex items-center gap-1 text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n\n {/* Price on image */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />\n </span>\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {stripHtmlToText(product.description)}\n </p>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[12px] text-muted-foreground\">\n <span className=\"w-[7px] h-[7px] rounded-full bg-emerald-500 animate-pulse\" />\n <span>Available</span>\n {hasDeposit && (\n <>\n <span className=\"text-border\">·</span>\n <span><Price amount={product.deposit_amount!} /> deposit</span>\n </>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Book now →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport { stripHtmlToText } from \"@cimplify/sdk/react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ServiceCardLayoutProps extends CardLayoutProps {\n slots?: AvailableSlot[];\n onBook?: (product: Product) => void;\n}\n\nfunction formatDuration(minutes: number, unit?: string): string {\n if (unit && unit !== \"minutes\") return `${minutes} ${unit}`;\n if (minutes >= 60) {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return m > 0 ? `${h}h ${m}m` : `${h} hr${h > 1 ? \"s\" : \"\"}`;\n }\n return `${minutes} min`;\n}\n\nexport function StandardServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const currency = getProductCurrency(product);\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Duration pill */}\n {product.duration_minutes != null && (\n <span className=\"absolute bottom-3 left-3 inline-flex items-center gap-1 text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n\n {/* Price on image */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} currency={currency} />\n </span>\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {stripHtmlToText(product.description)}\n </p>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[12px] text-muted-foreground\">\n <span className=\"w-[7px] h-[7px] rounded-full bg-emerald-500 animate-pulse\" />\n <span>Available</span>\n {hasDeposit && (\n <>\n <span className=\"text-border\">·</span>\n <span><Price amount={product.deposit_amount!} currency={currency} /> deposit</span>\n </>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Book now →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "cards/subscription-card.tsx",
|
|
14
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport { stripHtmlToText } from \"@cimplify/sdk/react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst FREQUENCY_LABELS: Record<string, string> = {\n weekly: \"/week\",\n biweekly: \"/2wk\",\n monthly: \"/mo\",\n quarterly: \"/qtr\",\n annually: \"/yr\",\n};\n\nexport function SubscriptionCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const plans = product.billing_plans || [];\n const primaryPlan = plans[0];\n const frequency = primaryPlan?.frequency\n ? FREQUENCY_LABELS[primaryPlan.frequency] || `/${primaryPlan.frequency}`\n : \"/mo\";\n const hasTrial = primaryPlan?.trial_days != null && primaryPlan.trial_days > 0;\n const tags = product.tags || [];\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Price on image */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />{frequency}\n </span>\n\n {/* Trial badge */}\n {hasTrial && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-emerald-50/90 text-emerald-700 border border-emerald-200/50 backdrop-blur px-2 py-0.5 rounded-md\">\n {primaryPlan!.trial_days}-day free trial\n </span>\n )}\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {stripHtmlToText(product.description)}\n </p>\n )}\n\n {/* Tags */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-2.5\">\n {tags.slice(0, 3).map((tag) => (\n <span key={tag} className=\"text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Billing plan options */}\n {plans.length > 1 && (\n <div className=\"mt-3 space-y-1\">\n {plans.slice(0, 3).map((plan) => {\n const label = plan.frequency\n ? plan.frequency.charAt(0).toUpperCase() + plan.frequency.slice(1)\n : \"Standard\";\n const hasMarkup = plan.markup_type && plan.markup_amount;\n const isSavings = plan.markup_type === \"percentage\" && plan.markup_amount != null && plan.markup_amount < 0;\n\n return (\n <div key={plan.id} className=\"flex items-center justify-between text-[11px]\">\n <span className=\"text-muted-foreground\">{label}</span>\n <span className=\"font-medium\">\n {hasMarkup && isSavings && (\n <span className=\"text-emerald-600 mr-1\">Save {Math.abs(plan.markup_amount!)}%</span>\n )}\n </span>\n </div>\n );\n })}\n </div>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[11px] text-muted-foreground\">\n {hasTrial && (\n <span className=\"text-emerald-600 font-medium\">Free trial</span>\n )}\n {primaryPlan?.setup_fee != null && primaryPlan.setup_fee > 0 && (\n <span><Price amount={primaryPlan.setup_fee} /> setup</span>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Subscribe →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport { stripHtmlToText } from \"@cimplify/sdk/react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst FREQUENCY_LABELS: Record<string, string> = {\n weekly: \"/week\",\n biweekly: \"/2wk\",\n monthly: \"/mo\",\n quarterly: \"/qtr\",\n annually: \"/yr\",\n};\n\nexport function SubscriptionCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const currency = getProductCurrency(product);\n const plans = product.billing_plans || [];\n const primaryPlan = plans[0];\n const frequency = primaryPlan?.frequency\n ? FREQUENCY_LABELS[primaryPlan.frequency] || `/${primaryPlan.frequency}`\n : \"/mo\";\n const hasTrial = primaryPlan?.trial_days != null && primaryPlan.trial_days > 0;\n const tags = product.tags || [];\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Price on image */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} currency={currency} />{frequency}\n </span>\n\n {/* Trial badge */}\n {hasTrial && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-emerald-50/90 text-emerald-700 border border-emerald-200/50 backdrop-blur px-2 py-0.5 rounded-md\">\n {primaryPlan!.trial_days}-day free trial\n </span>\n )}\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {stripHtmlToText(product.description)}\n </p>\n )}\n\n {/* Tags */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-2.5\">\n {tags.slice(0, 3).map((tag) => (\n <span key={tag} className=\"text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Billing plan options */}\n {plans.length > 1 && (\n <div className=\"mt-3 space-y-1\">\n {plans.slice(0, 3).map((plan) => {\n const label = plan.frequency\n ? plan.frequency.charAt(0).toUpperCase() + plan.frequency.slice(1)\n : \"Standard\";\n const hasMarkup = plan.markup_type && plan.markup_amount;\n const isSavings = plan.markup_type === \"percentage\" && plan.markup_amount != null && plan.markup_amount < 0;\n\n return (\n <div key={plan.id} className=\"flex items-center justify-between text-[11px]\">\n <span className=\"text-muted-foreground\">{label}</span>\n <span className=\"font-medium\">\n {hasMarkup && isSavings && (\n <span className=\"text-emerald-600 mr-1\">Save {Math.abs(plan.markup_amount!)}%</span>\n )}\n </span>\n </div>\n );\n })}\n </div>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[11px] text-muted-foreground\">\n {hasTrial && (\n <span className=\"text-emerald-600 font-medium\">Free trial</span>\n )}\n {primaryPlan?.setup_fee != null && primaryPlan.setup_fee > 0 && (\n <span><Price amount={primaryPlan.setup_fee} currency={currency} /> setup</span>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Subscribe →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "variant-selector.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useState, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { VariantView, VariantAxisWithValues } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface VariantSelectorClassNames {\n root?: string;\n axisLabel?: string;\n axisOptions?: string;\n option?: string;\n optionSelected?: string;\n listLabel?: string;\n list?: string;\n name?: string;\n pricing?: string;\n adjustment?: string;\n}\n\nexport interface VariantSelectorProps {\n variants: VariantView[];\n variantAxes?: VariantAxisWithValues[];\n basePrice?: Money;\n selectedVariantId?: string;\n onVariantChange: (variantId: string | undefined, variant: VariantView | undefined) => void;\n productName?: string;\n className?: string;\n classNames?: VariantSelectorClassNames;\n}\n\nexport function VariantSelector({\n variants,\n variantAxes,\n basePrice,\n selectedVariantId,\n onVariantChange,\n productName,\n className,\n classNames,\n}: VariantSelectorProps): React.ReactElement | null {\n const [axisSelections, setAxisSelections] = useState<Record<string, string>>({});\n const initialized = useRef(false);\n const idPrefix = useId();\n\n useEffect(() => {\n initialized.current = false;\n }, [variants]);\n\n useEffect(() => {\n if (initialized.current) return;\n if (!variants || variants.length === 0) return;\n\n const defaultVariant = variants.find((v) => v.is_default) || variants[0];\n if (!defaultVariant) return;\n\n initialized.current = true;\n onVariantChange(defaultVariant.id, defaultVariant);\n\n if (defaultVariant.display_attributes) {\n const initial: Record<string, string> = {};\n for (const attr of defaultVariant.display_attributes) {\n initial[attr.axis_id] = attr.value_id;\n }\n setAxisSelections(initial);\n }\n }, [variants, onVariantChange]);\n\n useEffect(() => {\n if (!initialized.current) return;\n if (!variantAxes || variantAxes.length === 0) return;\n\n const match = variants.find((v) => {\n if (!v.display_attributes) return false;\n return v.display_attributes.every(\n (attr) => axisSelections[attr.axis_id] === attr.value_id,\n );\n });\n\n if (match && match.id !== selectedVariantId) {\n onVariantChange(match.id, match);\n }\n }, [axisSelections, variants, variantAxes, selectedVariantId, onVariantChange]);\n\n if (!variants || variants.length <= 1) {\n return null;\n }\n\n const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;\n\n if (variantAxes && variantAxes.length > 0) {\n return (\n <div data-cimplify-variant-selector className={cn(\"space-y-5\", className, classNames?.root)}>\n {variantAxes.map((axis) => {\n const labelId = `${idPrefix}-axis-${axis.id}`;\n return (\n <div key={axis.id} data-cimplify-variant-axis>\n <label\n id={labelId}\n data-cimplify-variant-axis-label\n className={cn(\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\", classNames?.axisLabel)}\n >\n {axis.name}\n </label>\n <RadioGroup\n aria-labelledby={labelId}\n value={axisSelections[axis.id] ?? \"\"}\n onValueChange={(value) => {\n setAxisSelections((prev) => ({\n ...prev,\n [axis.id]: value,\n }));\n }}\n data-cimplify-variant-axis-options\n className={cn(\"flex flex-wrap gap-2\", classNames?.axisOptions)}\n >\n {axis.values.map((value) => {\n const isSelected = axisSelections[axis.id] === value.id;\n return (\n <Radio.Root\n key={value.id}\n value={value.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"px-4 py-2 border text-sm font-medium transition-colors border-border hover:border-primary/50\",\n isSelected && \"bg-primary text-primary-foreground border-primary\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n {value.name}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n })}\n </div>\n );\n }\n\n const listLabelId = `${idPrefix}-variant-list`;\n\n return (\n <div data-cimplify-variant-selector className={cn(className, classNames?.root)}>\n <div\n data-cimplify-variant-list-header\n className={cn(\"flex items-center justify-between py-3\", classNames?.listLabel)}\n >\n <label id={listLabelId} className=\"text-base font-bold\">\n Options\n </label>\n <span className=\"text-xs font-semibold text-destructive bg-destructive/10 px-2.5 py-1 rounded\">\n Required\n </span>\n </div>\n <RadioGroup\n aria-labelledby={listLabelId}\n value={selectedVariantId ?? \"\"}\n onValueChange={(value) => {\n const variant = variants.find((v) => v.id === value);\n onVariantChange(variant?.id, variant);\n }}\n data-cimplify-variant-list\n className={cn(\"divide-y divide-border\", classNames?.list)}\n >\n {variants.map((variant) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 py-4 transition-colors cursor-pointer\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <span\n data-cimplify-variant-radio\n className={cn(\n \"w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors\",\n isSelected ? \"border-primary\" : \"border-muted-foreground/30\",\n )}\n >\n {isSelected && <span className=\"w-2.5 h-2.5 rounded-full bg-primary\" />}\n </span>\n <span\n data-cimplify-variant-name\n className={cn(\"flex-1 min-w-0 text-sm\", classNames?.name)}\n >\n {getVariantDisplayName(variant, productName)}\n </span>\n <span data-cimplify-variant-pricing className={cn(\"text-sm text-muted-foreground\", classNames?.pricing)}>\n {adjustment > 0 ? \"+\" : adjustment < 0 ? \"\" : \"+\"}\n <Price amount={variant.price_adjustment} />\n </span>\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n}\n"
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { VariantView, VariantAxisWithValues } from \"@cimplify/sdk\";\nimport type { Money, CurrencyCode } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface VariantSelectorClassNames {\n root?: string;\n axisLabel?: string;\n axisOptions?: string;\n option?: string;\n optionSelected?: string;\n listLabel?: string;\n list?: string;\n name?: string;\n pricing?: string;\n adjustment?: string;\n}\n\nexport interface VariantSelectorProps {\n variants: VariantView[];\n variantAxes?: VariantAxisWithValues[];\n basePrice?: Money;\n currency?: CurrencyCode;\n selectedVariantId?: string;\n onVariantChange: (variantId: string | undefined, variant: VariantView | undefined) => void;\n productName?: string;\n className?: string;\n classNames?: VariantSelectorClassNames;\n}\n\nexport function VariantSelector({\n variants,\n variantAxes,\n basePrice,\n currency,\n selectedVariantId,\n onVariantChange,\n productName,\n className,\n classNames,\n}: VariantSelectorProps): React.ReactElement | null {\n const [axisSelections, setAxisSelections] = useState<Record<string, string>>({});\n const initialized = useRef(false);\n const idPrefix = useId();\n\n useEffect(() => {\n initialized.current = false;\n }, [variants]);\n\n useEffect(() => {\n if (initialized.current) return;\n if (!variants || variants.length === 0) return;\n\n const defaultVariant = variants.find((v) => v.is_default) || variants[0];\n if (!defaultVariant) return;\n\n initialized.current = true;\n onVariantChange(defaultVariant.id, defaultVariant);\n\n if (defaultVariant.display_attributes) {\n const initial: Record<string, string> = {};\n for (const attr of defaultVariant.display_attributes) {\n initial[attr.axis_id] = attr.value_id;\n }\n setAxisSelections(initial);\n }\n }, [variants, onVariantChange]);\n\n useEffect(() => {\n if (!initialized.current) return;\n if (!variantAxes || variantAxes.length === 0) return;\n\n const match = variants.find((v) => {\n if (!v.display_attributes) return false;\n return v.display_attributes.every(\n (attr) => axisSelections[attr.axis_id] === attr.value_id,\n );\n });\n\n if (match && match.id !== selectedVariantId) {\n onVariantChange(match.id, match);\n }\n }, [axisSelections, variants, variantAxes, selectedVariantId, onVariantChange]);\n\n if (!variants || variants.length <= 1) {\n return null;\n }\n\n const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;\n\n if (variantAxes && variantAxes.length > 0) {\n return (\n <div data-cimplify-variant-selector className={cn(\"space-y-5\", className, classNames?.root)}>\n {variantAxes.map((axis) => {\n const labelId = `${idPrefix}-axis-${axis.id}`;\n return (\n <div key={axis.id} data-cimplify-variant-axis>\n <label\n id={labelId}\n data-cimplify-variant-axis-label\n className={cn(\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\", classNames?.axisLabel)}\n >\n {axis.name}\n </label>\n <RadioGroup\n aria-labelledby={labelId}\n value={axisSelections[axis.id] ?? \"\"}\n onValueChange={(value) => {\n setAxisSelections((prev) => ({\n ...prev,\n [axis.id]: value,\n }));\n }}\n data-cimplify-variant-axis-options\n className={cn(\"flex flex-wrap gap-2\", classNames?.axisOptions)}\n >\n {axis.values.map((value) => {\n const isSelected = axisSelections[axis.id] === value.id;\n return (\n <Radio.Root\n key={value.id}\n value={value.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"px-4 py-2 border text-sm font-medium transition-colors border-border hover:border-primary/50\",\n isSelected && \"bg-primary text-primary-foreground border-primary\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n {value.name}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n })}\n </div>\n );\n }\n\n const listLabelId = `${idPrefix}-variant-list`;\n\n return (\n <div data-cimplify-variant-selector className={cn(className, classNames?.root)}>\n <div\n data-cimplify-variant-list-header\n className={cn(\"flex items-center justify-between py-3\", classNames?.listLabel)}\n >\n <label id={listLabelId} className=\"text-base font-bold\">\n Options\n </label>\n <span className=\"text-xs font-semibold text-destructive bg-destructive/10 px-2.5 py-1 rounded\">\n Required\n </span>\n </div>\n <RadioGroup\n aria-labelledby={listLabelId}\n value={selectedVariantId ?? \"\"}\n onValueChange={(value) => {\n const variant = variants.find((v) => v.id === value);\n onVariantChange(variant?.id, variant);\n }}\n data-cimplify-variant-list\n className={cn(\"divide-y divide-border\", classNames?.list)}\n >\n {variants.map((variant) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 py-4 transition-colors cursor-pointer\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <span\n data-cimplify-variant-radio\n className={cn(\n \"w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors\",\n isSelected ? \"border-primary\" : \"border-muted-foreground/30\",\n )}\n >\n {isSelected && <span className=\"w-2.5 h-2.5 rounded-full bg-primary\" />}\n </span>\n <span\n data-cimplify-variant-name\n className={cn(\"flex-1 min-w-0 text-sm\", classNames?.name)}\n >\n {getVariantDisplayName(variant, productName)}\n </span>\n <span data-cimplify-variant-pricing className={cn(\"text-sm text-muted-foreground\", classNames?.pricing)}>\n {adjustment > 0 ? \"+\" : adjustment < 0 ? \"\" : \"+\"}\n <Price amount={variant.price_adjustment} currency={currency} />\n </span>\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"path": "volume-pricing.tsx",
|
|
13
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { QuantityPricingTier } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface VolumePricingClassNames {\n root?: string;\n trigger?: string;\n triggerIcon?: string;\n panel?: string;\n tier?: string;\n tierActive?: string;\n tierRange?: string;\n tierPrice?: string;\n}\n\nexport interface VolumePricingProps {\n tiers: QuantityPricingTier[];\n currentQuantity?: number;\n defaultOpen?: boolean;\n className?: string;\n classNames?: VolumePricingClassNames;\n}\n\nfunction formatRange(tier: QuantityPricingTier): string {\n if (tier.max_quantity != null) {\n return `${tier.min_quantity}–${tier.max_quantity} units`;\n }\n return `${tier.min_quantity}+ units`;\n}\n\nfunction isActiveTier(tier: QuantityPricingTier, quantity: number): boolean {\n if (quantity < tier.min_quantity) return false;\n if (tier.max_quantity != null && quantity > tier.max_quantity) return false;\n return true;\n}\n\nexport function VolumePricing({\n tiers,\n currentQuantity,\n defaultOpen = false,\n className,\n classNames,\n}: VolumePricingProps): React.ReactElement | null {\n if (tiers.length < 2) return null;\n\n const sorted = [...tiers].sort((a, b) => a.min_quantity - b.min_quantity);\n\n return (\n <details\n open={defaultOpen || undefined}\n data-cimplify-volume-pricing\n className={cn(\"border border-border\", className, classNames?.root)}\n >\n <summary\n data-cimplify-volume-pricing-trigger\n className={cn(\n \"flex items-center justify-between px-4 py-3 cursor-pointer select-none text-sm font-medium list-none [&::-webkit-details-marker]:hidden\",\n classNames?.trigger,\n )}\n >\n Volume pricing\n <svg\n viewBox=\"0 0 12 12\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\n \"w-3.5 h-3.5 text-muted-foreground transition-transform [[open]>&]:rotate-180\",\n classNames?.triggerIcon,\n )}\n >\n <path d=\"M2 4.5l4 4 4-4\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </summary>\n <div\n data-cimplify-volume-pricing-panel\n className={cn(\"border-t border-border divide-y divide-border\", classNames?.panel)}\n >\n {sorted.map((tier, i) => {\n const active = currentQuantity != null && isActiveTier(tier, currentQuantity);\n return (\n <div\n key={i}\n data-cimplify-volume-pricing-tier\n data-active={active || undefined}\n className={cn(\n \"flex items-center justify-between px-4 py-2.5 text-sm\",\n active\n ? cn(\"bg-primary/5 font-medium\", classNames?.tierActive)\n : classNames?.tier,\n )}\n >\n <span\n data-cimplify-volume-pricing-range\n className={cn(\"text-muted-foreground\", classNames?.tierRange)}\n >\n {formatRange(tier)}\n </span>\n <span data-cimplify-volume-pricing-price className={classNames?.tierPrice}>\n <Price amount={tier.unit_price} prefix=\"\" /> /ea\n </span>\n </div>\n );\n })}\n </div>\n </details>\n );\n}\n"
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { QuantityPricingTier } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface VolumePricingClassNames {\n root?: string;\n trigger?: string;\n triggerIcon?: string;\n panel?: string;\n tier?: string;\n tierActive?: string;\n tierRange?: string;\n tierPrice?: string;\n}\n\nexport interface VolumePricingProps {\n tiers: QuantityPricingTier[];\n currentQuantity?: number;\n defaultOpen?: boolean;\n currency?: CurrencyCode;\n className?: string;\n classNames?: VolumePricingClassNames;\n}\n\nfunction formatRange(tier: QuantityPricingTier): string {\n if (tier.max_quantity != null) {\n return `${tier.min_quantity}–${tier.max_quantity} units`;\n }\n return `${tier.min_quantity}+ units`;\n}\n\nfunction isActiveTier(tier: QuantityPricingTier, quantity: number): boolean {\n if (quantity < tier.min_quantity) return false;\n if (tier.max_quantity != null && quantity > tier.max_quantity) return false;\n return true;\n}\n\nexport function VolumePricing({\n tiers,\n currentQuantity,\n defaultOpen = false,\n currency,\n className,\n classNames,\n}: VolumePricingProps): React.ReactElement | null {\n if (tiers.length < 2) return null;\n\n const sorted = [...tiers].sort((a, b) => a.min_quantity - b.min_quantity);\n\n return (\n <details\n open={defaultOpen || undefined}\n data-cimplify-volume-pricing\n className={cn(\"border border-border\", className, classNames?.root)}\n >\n <summary\n data-cimplify-volume-pricing-trigger\n className={cn(\n \"flex items-center justify-between px-4 py-3 cursor-pointer select-none text-sm font-medium list-none [&::-webkit-details-marker]:hidden\",\n classNames?.trigger,\n )}\n >\n Volume pricing\n <svg\n viewBox=\"0 0 12 12\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\n \"w-3.5 h-3.5 text-muted-foreground transition-transform [[open]>&]:rotate-180\",\n classNames?.triggerIcon,\n )}\n >\n <path d=\"M2 4.5l4 4 4-4\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </summary>\n <div\n data-cimplify-volume-pricing-panel\n className={cn(\"border-t border-border divide-y divide-border\", classNames?.panel)}\n >\n {sorted.map((tier, i) => {\n const active = currentQuantity != null && isActiveTier(tier, currentQuantity);\n return (\n <div\n key={i}\n data-cimplify-volume-pricing-tier\n data-active={active || undefined}\n className={cn(\n \"flex items-center justify-between px-4 py-2.5 text-sm\",\n active\n ? cn(\"bg-primary/5 font-medium\", classNames?.tierActive)\n : classNames?.tier,\n )}\n >\n <span\n data-cimplify-volume-pricing-range\n className={cn(\"text-muted-foreground\", classNames?.tierRange)}\n >\n {formatRange(tier)}\n </span>\n <span data-cimplify-volume-pricing-price className={classNames?.tierPrice}>\n <Price amount={tier.unit_price} currency={currency} prefix=\"\" /> /ea\n </span>\n </div>\n );\n })}\n </div>\n </details>\n );\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "cards/wholesale-product-card.tsx",
|
|
14
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage, QuickAddButton } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { PriceRange } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function WholesaleProductCard({\n product,\n onQuickAdd,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const status = product.inventory_status;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"square\" renderImage={renderImage}>\n {/* MOQ badge */}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-zinc-800 text-white px-2 py-0.5 rounded-md\">\n MOQ: {product.min_order_quantity}\n </span>\n )}\n\n <QuickAddButton\n onClick={() => onQuickAdd?.(product)}\n className=\"absolute bottom-3 right-3\"\n />\n </CardImage>\n\n <div className=\"p-3.5\">\n {/* SKU */}\n {product.sku && (\n <p className=\"text-[10px] text-muted-foreground font-mono\">\n {product.sku}\n </p>\n )}\n\n {/* Name */}\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight mt-1 truncate\">\n {product.name}\n </h3>\n\n {/* Price range or single */}\n <div className=\"mt-2\">\n {hasTiers ? (\n <PriceRange product={product} className=\"text-sm font-bold\" />\n ) : (\n <Price amount={product.default_price} className=\"text-sm font-bold\" />\n )}\n </div>\n\n {/* Stock */}\n {status && (\n <div className=\"flex items-center gap-1.5 mt-2\">\n <span className={cn(\n \"w-[7px] h-[7px] rounded-full shrink-0\",\n status.in_stock\n ? status.low_stock ? \"bg-amber-500\" : \"bg-emerald-500\"\n : \"bg-red-500\",\n )} />\n <span className={cn(\n \"text-[10.5px] font-medium\",\n status.in_stock\n ? status.low_stock ? \"text-amber-600\" : \"text-emerald-600\"\n : \"text-red-600\",\n )}>\n {!status.in_stock\n ? \"Out of stock\"\n : status.stock_level != null\n ? `${status.stock_level.toLocaleString()} in stock`\n : \"In stock\"\n }\n </span>\n </div>\n )}\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage, QuickAddButton } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { PriceRange } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function WholesaleProductCard({\n product,\n onQuickAdd,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const currency = getProductCurrency(product);\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const status = product.inventory_status;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"square\" renderImage={renderImage}>\n {/* MOQ badge */}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-zinc-800 text-white px-2 py-0.5 rounded-md\">\n MOQ: {product.min_order_quantity}\n </span>\n )}\n\n <QuickAddButton\n onClick={() => onQuickAdd?.(product)}\n className=\"absolute bottom-3 right-3\"\n />\n </CardImage>\n\n <div className=\"p-3.5\">\n {/* SKU */}\n {product.sku && (\n <p className=\"text-[10px] text-muted-foreground font-mono\">\n {product.sku}\n </p>\n )}\n\n {/* Name */}\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight mt-1 truncate\">\n {product.name}\n </h3>\n\n {/* Price range or single */}\n <div className=\"mt-2\">\n {hasTiers ? (\n <PriceRange product={product} className=\"text-sm font-bold\" />\n ) : (\n <Price amount={product.default_price} currency={currency} className=\"text-sm font-bold\" />\n )}\n </div>\n\n {/* Stock */}\n {status && (\n <div className=\"flex items-center gap-1.5 mt-2\">\n <span className={cn(\n \"w-[7px] h-[7px] rounded-full shrink-0\",\n status.in_stock\n ? status.low_stock ? \"bg-amber-500\" : \"bg-emerald-500\"\n : \"bg-red-500\",\n )} />\n <span className={cn(\n \"text-[10.5px] font-medium\",\n status.in_stock\n ? status.low_stock ? \"text-amber-600\" : \"text-emerald-600\"\n : \"text-red-600\",\n )}>\n {!status.in_stock\n ? \"Out of stock\"\n : status.stock_level != null\n ? `${status.stock_level.toLocaleString()} in stock`\n : \"In stock\"\n }\n </span>\n </div>\n )}\n </div>\n </CardShell>\n );\n}\n"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
|
17
17
|
"path": "layouts/wholesale-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 { 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
|
+
"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 { getProductCurrency } from \"@cimplify/sdk\";\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 currency = getProductCurrency(product);\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} currency={currency} className=\"text-2xl font-extrabold\" />\n ) : (\n <Price amount={product.default_price} currency={currency} 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"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
}
|