import {ComponentMeta} from "../ComponentsMeta"; import React from "react"; import {iconWithClassName} from "../IconWithClassName"; import {CardRenderer} from "../CardRenderers"; import {__} from "../../globals"; import {usingOfferOptions, withPrefix} from "./CardHelpers"; import {FieldType} from "../fields"; import {NumberProps} from "../fields/Number"; import {ComponentCategory} from "../components"; import {CardItemSecondaryValue} from "../CardItemSecondaryValue"; import {DiscountValue} from "./DiscountValue"; import {ListItem} from "./ListItem"; import {BaseOfferOptions} from "./OfferTypes"; import {RepeatingLabel} from "./Recusrive"; import {RowOptions} from "./Fields/RowOptions"; import {getBOGOOfferNumberInputProps} from "../fields/NumberFieldUtils"; export type qtyType = 'simple' | 'advanced' export type Qty = { "type": "any", // currently only 'any' is supported "amount": 1 // this is a placeholder for future advanced types } export type ProductDiscountOptions = { // new added in 1.1 qtyType: qtyType, "qty": Qty, quantity: number, discount: { type: 'free' | 'percentage' | 'amount' | 'fixed-price', amount: number }, missingItemsInCart: { mode: 'auto-add' | 'manual-add' } } & BaseOfferOptions // Safe interpolation utility to inject JSX elements into translated templates like __('{1} for {2}') // Only replaces exact numeric placeholder tokens; other text stays as plain text nodes. const interpolateElements = (template: string, replacements: Record): React.ReactNode[] => { return template .split(/(\{\d+\})/g) .filter(Boolean) .map((token, i) => { if (/^\{\d+\}$/.test(token)) { const index = parseInt(token.slice(1, -1), 10); return {replacements[index]}; } return {token}; }); }; export type BuyXGetXOptions = ProductDiscountOptions & { quantityType: 'fixed' | 'duplicate', missingItemsInCart: { mode: 'auto-add' | 'manual-add' }, } export const type = 'BuyXGetX' export const BuyXGetXMeta: ComponentMeta = { id: type, type: 'offers', fieldsTitlePrefix: false, category: ComponentCategory.BOGO, listTitle: withPrefix(__('BOGO:')), titlePrefix: {__('BOGO:')}, icon: iconWithClassName(({className, context}) => { if (context === 'card') { // solid return } // outline return }), fieldsMap: ({options}) => { return ([ [ new RowOptions({ type: 'featured', colWidth: 'auto' }), options.quantityType === 'fixed' && { id: 'quantity', renderType: FieldType.Number, fieldProps: { hasAmountStructure: false, labels: { input: { inputLeftOutside: {__('Get')} } }, border: false } as Partial, }, { id: 'discount', renderType: FieldType.Number, fieldProps: getBOGOOfferNumberInputProps(options.discount.type) as Partial, }, ], options.quantityType === 'fixed' && [ { title: __('When items eligible for discount aren\'t in the cart'), id: 'missingItemsInCart.mode', renderType: FieldType.Tab, fieldOptions: { direction: 'vertical', } } ], ]) }, fieldsMapLast: ({options}) => [ [ [ { id: 'quantityType', title: __('Experimental mode'), renderType: FieldType.Tab, fieldOptions: { direction: 'vertical', } } ] ] ], fieldsNotes: ({options}) => options.quantityType === 'fixed' ? [ __('This offer uses the filtered items as the qualifier for the offer. This means the items matching your filters are what a customer must buy to unlock the offer, and they will not be discounted themselves. The discount will be applied to the cheapest additional items of the same type in the cart.'), __('For example: If you create a "Buy 1, Get 1 Free" offer for Category A. If a customer adds just one $25 shirt from this category, the offer will automatically add a second, identical $25 shirt to the cart and make it free. However, if the customer manually adds 2 qualifying shirts before applying the coupon, no extra item will be added and the discount will be applied to the cheapest'), /*__('Setting a quantity in your filters is required for this offer')*/ ] : [] } export const BuyXGetXRenderer: CardRenderer = { type: type, prefix: "*", example: __('Buy 1 t-shirt, get 1 free, Buy 2 t-shirts, get 1 at 50% off'), icon: BuyXGetXMeta.icon!, AboveComputedValue: () => false, // No above computed value for this card suggestedScopeMarker: (suggestedScope) => { if (suggestedScope === 'bogo') { return { prefix: __('BOGO'), label: __('Get') } } }, ComputedValue: usingOfferOptions((options, {color,}) => { const {quantity, discount} = options return
{false && -}
{__('Get')}
{options.quantityType === 'fixed' ?
{quantity}
:
{__('x2')}
}
{false && -} {false && color.text(20) }}>{__('Get')}} {
{/* {interpolateElements(__(' {1} for {2} '), { 1: renderOfferMainValue(quantity), 2: renderDiscountValue(), })} */}
}
}), OutsideComputedValue: usingOfferOptions(({quantityType, quantity, discount, missingItemsInCart, __BASE__: {isRecursive}}, {color,}) => { return
    {quantityType === 'fixed' && {(quantity === 1 ? __('* extra item of the same product type') : __('* extra items of the same product type')).replace('*', quantity)} {__(' (Cheapest)')} } {quantityType === 'duplicate' && {__('Matches quantity of eligible items.')} } {missingItemsInCart.mode === 'auto-add' || quantityType === 'duplicate'? __('Automatically added') : __('Manually added')} {isRecursive && }
}), }