diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundle/BundleCTA.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundle/BundleCTA.tsx index 9f136d91b..72f96f46e 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundle/BundleCTA.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundle/BundleCTA.tsx @@ -102,8 +102,8 @@ export interface BundleSelectorSubProps { icon: Partial; } -interface BundledCTAProps extends ComponentProps { - ctaSlot?: JSX.Element; +export interface BundledCTAProps extends ComponentProps { + ctaSlot?: JSX.Element | React.FunctionComponent; cartStore: CartStore; onAddToCart: (e: React.MouseEvent) => void; ctaIcon?: IconType | Partial | false; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.stories.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.stories.tsx index d1ca09d6a..d74c79116 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.stories.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.stories.tsx @@ -16,6 +16,7 @@ import type { RecommendationControllerConfig } from '@searchspring/snap-controll export default { title: 'Templates/RecommendationBundle', component: RecommendationBundle, + tags: ['autodocs'], parameters: { docs: { page: () => ( diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.tsx index f4ce9c389..54aee5e3d 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundle/RecommendationBundle.tsx @@ -16,7 +16,7 @@ import { IconProps, IconType } from '../../Atoms/Icon'; import type { RecommendationController } from '@searchspring/snap-controller'; import type { Product } from '@searchspring/snap-store-mobx'; import { BundleSelector } from './BundleSelector'; -import { BundledCTA } from './BundleCTA'; +import { BundledCTA, BundledCTAProps } from './BundleCTA'; import { Lang } from '../../../hooks'; import { useIntersection } from '../../../hooks'; @@ -664,7 +664,12 @@ export interface RecommendationBundleProps extends ComponentProps { onAddToCart: (e: MouseEvent, items: Product[]) => void; title?: JSX.Element | string; breakpoints?: BreakpointsProps; - resultComponent?: ResultComponent<{ seed?: boolean; selected?: boolean; onProductSelect?: (product: Product) => void }>; + resultComponent?: ResultComponent<{ + controller: RecommendationController; + seed?: boolean; + selected?: boolean; + onProductSelect?: (product: Product) => void; + }>; preselectedCount?: number; hideCheckboxes?: boolean; hideSeed?: boolean; @@ -676,7 +681,7 @@ export interface RecommendationBundleProps extends ComponentProps { ctaButtonText?: string; ctaButtonSuccessText?: string; ctaButtonSuccessTimeout?: number; - ctaSlot?: JSX.Element; + ctaSlot?: JSX.Element | React.FunctionComponent; vertical?: boolean; carousel?: BundleCarouselProps; slidesPerView?: number; // TODO: remove this prop? diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.stories.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.stories.tsx new file mode 100644 index 000000000..71e9ee6a1 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.stories.tsx @@ -0,0 +1,238 @@ +import { h } from 'preact'; + +import { ArgsTable, PRIMARY_STORY, Markdown } from '@storybook/blocks'; + +import { RecommendationBundleEasyAdd, RecommendationBundleEasyAddProps } from './RecommendationBundleEasyAdd'; +import { componentArgs, highlightedCode } from '../../../utilities'; +import { Snapify } from '../../../utilities/snapify'; + +import Readme from './readme.md'; +import type { RecommendationController } from '@searchspring/snap-controller'; +import type { Product } from '@searchspring/snap-store-mobx'; +import type { Next } from '@searchspring/snap-event-manager'; +import { iconPaths } from '../../Atoms/Icon'; +import type { RecommendationControllerConfig } from '@searchspring/snap-controller'; + +export default { + title: 'Templates/RecommendationBundleEasyAdd', + component: RecommendationBundleEasyAdd, + tags: ['autodocs'], + parameters: { + docs: { + page: () => ( +
+ + {Readme} + + +
+ ), + }, + }, + decorators: [ + (Story: any) => ( +
+ +
+ ), + ], + argTypes: { + controller: { + description: 'Controller reference', + type: { required: true }, + table: { + type: { + summary: 'Controller', + }, + }, + control: { type: 'none' }, + }, + results: { + description: 'Results store reference, overrides controller.store.results', + type: { required: false }, + table: { + type: { + summary: 'Results store object', + }, + }, + control: { type: 'none' }, + }, + resultComponent: { + description: 'Slot for custom result component', + table: { + type: { + summary: 'component', + }, + }, + }, + title: { + description: 'recommendation title', + table: { + type: { + summary: 'string | JSX Element', + }, + defaultValue: { summary: '' }, + }, + control: { type: 'text' }, + }, + onAddToCart: { + description: 'onClick event handler for add bundle to cart button in CTA', + type: { required: true }, + table: { + type: { + summary: 'function', + }, + }, + action: 'onAddToCart', + }, + seedText: { + description: 'Text to render in seed product badge', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Seed Product' }, + }, + control: { type: 'text' }, + }, + vertical: { + description: 'set the recommendation to render vertically', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: true }, + }, + control: { type: 'boolean' }, + }, + ctaButtonText: { + description: 'text to render in add to cart button', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Add All To Cart' }, + }, + control: { type: 'text' }, + }, + ctaIcon: { + desciption: 'The `ctaIcon` prop specifies the icon to render in the CTA. Takes an object with `Icon` component props or a string.', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'bag' }, + }, + control: { + type: 'select', + options: [...Object.keys(iconPaths)], + }, + }, + ctaButtonSuccessText: { + description: 'text to temporarily render in the add to cart button after it is clicked', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Bundle Added!' }, + }, + control: { type: 'text' }, + }, + ctaButtonSuccessTimeout: { + description: 'Number of ms to show success text in add to cart button before reverting back to normal text', + defaultValue: 2000, + table: { + type: { + summary: 'number', + }, + }, + control: { type: 'number' }, + }, + ctaInline: { + description: 'boolean to enable the stacked add to cart button display', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: true }, + }, + control: { type: 'boolean' }, + }, + ctaSlot: { + description: 'Slot for custom add to cart component', + table: { + type: { + summary: 'component', + }, + }, + }, + lazyRender: { + description: 'Lazy render settings object', + defaultValue: { + enabled: true, + offset: '10%', + }, + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Lazy render settings object' }, + }, + control: { type: 'object' }, + }, + breakpoints: { + defaultValue: undefined, + description: 'Recommendation title', + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Breakpoint object' }, + }, + control: { type: 'object' }, + }, + ...componentArgs, + }, +}; + +const config: RecommendationControllerConfig = { + id: 'RecommendationBundle', + tag: 'bundle', + globals: { + siteId: '8uyt2m', + products: ['C-AD-W1-1869P'], + }, +}; + +const snapInstance = Snapify.recommendation(config); + +export const Default = ( + props: RecommendationBundleEasyAddProps, + { loaded: { controller } }: { loaded: { controller: RecommendationController } } +) => { + return ; +}; + +Default.loaders = [ + async () => { + snapInstance.on('afterStore', async ({ controller }: { controller: RecommendationController }, next: Next) => { + controller.store.results.forEach((result: Product) => (result.mappings.core!.url = 'javascript:void(0);')); + await next(); + }); + await snapInstance.search(); + return { + controller: snapInstance, + }; + }, +]; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx new file mode 100644 index 000000000..e23b5fe24 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx @@ -0,0 +1,74 @@ +import { h } from 'preact'; +import { css } from '@emotion/react'; +import { observer } from 'mobx-react'; + +import { defined, mergeProps } from '../../../utilities'; +import { Theme, useTheme } from '../../../providers'; +import { ComponentProps, StylingCSS } from '../../../types'; +import { RecommendationBundle, RecommendationBundleProps } from '../RecommendationBundle'; + +const CSS = { + RecommendationBundleEasyAdd: ({}: Partial) => + css({ + '.ss__recommendation-bundle__wrapper__cta': { + textAlign: 'center', + }, + }), +}; + +export const RecommendationBundleEasyAdd = observer((properties: RecommendationBundleEasyAddProps): JSX.Element => { + const globalTheme: Theme = useTheme(); + const defaultProps: Partial = {}; + + const props = mergeProps('recommendationBundleEasyAdd', globalTheme, defaultProps, properties); + + const { treePath, styleScript, theme, style, disableStyles, ...additionalProps } = props; + + const subProps: RecommendationBundleEasyAddSubProps = { + recommendationBundle: { + // default props + className: 'ss__recommendation-bundle-easy-add', + hideCheckboxes: true, + seedText: '', + ctaButtonText: 'Add Both', + ctaInline: false, + hideSeed: true, + vertical: true, + limit: 1, + carousel: { + enabled: false, + }, + separatorIcon: false, + // inherited props + ...defined({ + disableStyles, + }), + // component theme overrides + theme: props?.theme, + treePath, + }, + }; + + const styling: { css?: StylingCSS } = {}; + const stylingProps = { ...props, theme }; + + if (styleScript && !disableStyles) { + styling.css = [styleScript(stylingProps), style]; + } else if (!disableStyles) { + styling.css = [CSS.RecommendationBundleEasyAdd(stylingProps), style]; + } else if (style) { + styling.css = [style]; + } + + return ; +}); + +export type RecommendationBundleEasyAddProps = Omit< + RecommendationBundleProps, + 'hideSeed' | 'limit' | 'hideCheckboxes' | 'carousel' | 'separatorIcon' | 'separatorIconSeedOnly' | 'preselectedCount' +> & + ComponentProps; + +interface RecommendationBundleEasyAddSubProps { + recommendationBundle: Partial; +} diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/index.ts b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/index.ts new file mode 100644 index 000000000..d3e22b3f0 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/index.ts @@ -0,0 +1 @@ +export * from './RecommendationBundleEasyAdd'; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/readme.md b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/readme.md new file mode 100644 index 000000000..66cf50c63 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/readme.md @@ -0,0 +1,170 @@ +# RecommendationBundleEasyAdd + +Renders a single recommended product and bundle CTA section that includes a visually hidden seed product. Like all of the bundle components, the first result passed will be treated as the seed. The Seed will not be rendered, but will be included in the cartstore, bundle pricing, and items included when adding to the cart. + +## Sub-components +- RecommendationBundle + + +### controller +The required `controller` prop specifies a reference to the RecommendationController + +```jsx +{console.log(items)}} /> +``` + +### onAddToCart +the required `onAddToCart` prop sets a the callback function for when a add to cart button is clicked. This function will be passed an array of selected item ids and their quantities. + +```jsx +{console.log(items)}} /> +``` + +### results +The `results` prop specifies a reference to the results store array to use instead of the default `controller.store.results`. Note the first result will be displayed as the `seed` product. + +```jsx +{console.log(items)}} results={controller.store.results} /> +``` + +### title +The `title` prop specifies the carousel title + +```jsx +{console.log(items)}} title={'Recommended Bundle'} /> +``` + +### resultComponent +The `resultComponent` prop allows for a custom result component to be rendered. This component will be passed the following props - + +```jsx + { + result: Product, + seed: boolean, + selected: boolean, + onProductSelect: (result:Product) => void + } +``` + +```jsx +{console.log(items)}} resultComponent={} /> +``` + +### ctaButtonText +The `ctaButtonText` prop specifies the inner text to render in the add to cart button. + +```jsx +{console.log(items)}} ctaButtonText={'Add Bundle'} /> +``` + +### ctaButtonSuccessText +The `ctaButtonSuccessText` prop specifies text to temporarily render in the add to cart button after it is clicked. + +```jsx +{console.log(items)}} ctaButtonSuccessText={'Thanks for Shopping!'} /> +``` + +### ctaButtonSuccessTimeout +The `ctaButtonSuccessTimeout` prop specifies number of ms to show success text in add to cart button before reverting back to normal text + +```jsx +{console.log(items)}} ctaButtonSuccessTimeout={1500} /> +``` + +### ctaIcon +The `ctaIcon` prop specifies the icon to render in the CTA. Takes an object with `Icon` component props or a string. + +```jsx +{console.log(items)}} ctaIcon={'bag'} /> +``` + +### ctaInline +The `ctaInline` prop specifies if the add to cart display should be block or inline witht the carousel. + +```jsx +{console.log(items)}} ctaInline={true} /> +``` + +### ctaSlot +The `ctaSlot` prop allows for a custom add to cart cta component to be rendered. This component will be passed the following props - + +```jsx + { + cartStore: CartStore; + onclick: (e:any) => void + } +``` + +```jsx +{console.log(items)}} ctaSlot={} /> +``` + +### seedText +The `seedText` prop specifies text to be rendered as a badge in the seed product. + +```jsx +{console.log(items)}} seedText={"Main Product"} /> +``` + +### vertical +The `vertical` prop sets the carousel scroll direction to vertical. + +```jsx +{console.log(items)}} vertical={true} /> +``` + +### lazyRender +The `lazyRender` prop specifies an object of lazy rendering settings. The settings include an `enable` toggle (defaults to `true`) as well as an `offset` (default `"10%"`) to specify at what distance the component should start rendering relative to the bottom of the viewport. + +```jsx +const customLazyRenderProps = { + enabled: true, + offset: "20px" // any css margin values accepted - px, %, etc... +} + +{console.log(items)}} /> +``` + +### breakpoints +An object that modifies the responsive behavior of the carousel at various viewports. + +The object key specified the viewport for when the parameters will be applied. + +The default configuration contains the following properties, however **`any BundleRecommendation props`**, or [Swiper API parameters](https://swiperjs.com/react#swiper-props) can also be specified. + +`slidesPerView` - number of products to display per page + +`slidesPerGroup` - number of products to scroll by when next/previous button is clicked + +`spaceBetween` - spacing between each product + +```typescript +const defaultRecommendationBreakpoints = { + 0: { + carousel: { + enabled: false, + }, + limit: 2 + }, + 768: { + slidesPerView: 3, + slidesPerGroup: 3, + spaceBetween: 10, + }, + 1024: { + slidesPerView: 3, + slidesPerGroup: 3, + spaceBetween: 10, + }, + 1200: { + slidesPerView: 4, + slidesPerGroup: 4, + spaceBetween: 10, + }, +}; +``` + +```jsx +{console.log(items)}} breakpoints={defaultRecommendationBreakpoints} /> +``` + diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.stories.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.stories.tsx new file mode 100644 index 000000000..0189e9747 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.stories.tsx @@ -0,0 +1,259 @@ +import { h } from 'preact'; + +import { ArgsTable, PRIMARY_STORY, Markdown } from '@storybook/blocks'; + +import { RecommendationBundleList, RecommendationBundleListProps } from './RecommendationBundleList'; +import { componentArgs, highlightedCode } from '../../../utilities'; +import { Snapify } from '../../../utilities/snapify'; + +import Readme from './readme.md'; +import type { RecommendationController } from '@searchspring/snap-controller'; +import type { Product } from '@searchspring/snap-store-mobx'; +import type { Next } from '@searchspring/snap-event-manager'; +import type { RecommendationControllerConfig } from '@searchspring/snap-controller'; +import { iconPaths } from '../../Atoms/Icon'; + +export default { + title: 'Templates/RecommendationBundleList', + component: RecommendationBundleList, + tags: ['autodocs'], + parameters: { + docs: { + page: () => ( +
+ + {Readme} + + +
+ ), + }, + }, + decorators: [ + (Story: any) => ( +
+ +
+ ), + ], + argTypes: { + controller: { + description: 'Controller reference', + type: { required: true }, + table: { + type: { + summary: 'Controller', + }, + }, + control: { type: 'none' }, + }, + results: { + description: 'Results store reference, overrides controller.store.results', + type: { required: false }, + table: { + type: { + summary: 'Results store object', + }, + }, + control: { type: 'none' }, + }, + resultComponent: { + description: 'Slot for custom result component', + table: { + type: { + summary: 'component', + }, + }, + }, + title: { + description: 'recommendation title', + table: { + type: { + summary: 'string | JSX Element', + }, + defaultValue: { summary: '' }, + }, + control: { type: 'text' }, + }, + onAddToCart: { + description: 'onClick event handler for add bundle to cart button in CTA', + type: { required: true }, + table: { + type: { + summary: 'function', + }, + }, + action: 'onAddToCart', + }, + limit: { + description: 'limit the number of results rendered', + table: { + type: { + summary: 'number', + }, + }, + control: { type: 'number' }, + }, + carousel: { + description: 'Carousel settings object', + defaultValue: { + enabled: true, + loop: false, + }, + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Carousel settings object' }, + }, + control: { type: 'object' }, + }, + hideSeed: { + description: 'Hide/show seed result', + defaultValue: false, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: false }, + }, + control: { type: 'boolean' }, + }, + separatorIconSeedOnly: { + description: 'boolean to only have seperator Icon for the seed product', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: true }, + }, + control: { type: 'boolean' }, + }, + separatorIcon: { + defaultValue: 'plus', + description: 'Icon to render between results', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'plus' }, + }, + control: { + type: 'select', + options: [...Object.keys(iconPaths)], + }, + }, + preselectedCount: { + description: 'Number of results to have selected by default. (seed included)', + table: { + type: { + summary: 'number', + }, + }, + control: { type: 'number' }, + }, + ctaButtonText: { + description: 'text to render in add to cart button', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Add All To Cart' }, + }, + control: { type: 'text' }, + }, + ctaButtonSuccessTimeout: { + description: 'Number of ms to show success text in add to cart button before reverting back to normal text', + defaultValue: 2000, + table: { + type: { + summary: 'number', + }, + }, + control: { type: 'number' }, + }, + ctaButtonSuccessText: { + description: 'text to temporarily render in the add to cart button after it is clicked', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Bundle Added!' }, + }, + control: { type: 'text' }, + }, + ctaSlot: { + description: 'Slot for custom add to cart component', + table: { + type: { + summary: 'component', + }, + }, + }, + lazyRender: { + description: 'Lazy render settings object', + defaultValue: { + enabled: true, + offset: '10%', + }, + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Lazy render settings object' }, + }, + control: { type: 'object' }, + }, + breakpoints: { + defaultValue: undefined, + description: 'Recommendation title', + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Breakpoint object' }, + }, + control: { type: 'object' }, + }, + ...componentArgs, + }, +}; + +const config: RecommendationControllerConfig = { + id: 'RecommendationBundle', + tag: 'bundle', + globals: { + siteId: '8uyt2m', + products: ['C-AD-W1-1869P'], + }, +}; + +const snapInstance = Snapify.recommendation(config); + +export const Default = (props: RecommendationBundleListProps, { loaded: { controller } }: { loaded: { controller: RecommendationController } }) => { + return ; +}; + +Default.loaders = [ + async () => { + snapInstance.on('afterStore', async ({ controller }: { controller: RecommendationController }, next: Next) => { + controller.store.results.forEach((result: Product) => (result.mappings.core!.url = 'javascript:void(0);')); + await next(); + }); + await snapInstance.search(); + return { + controller: snapInstance, + }; + }, +]; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx new file mode 100644 index 000000000..d94f180c4 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx @@ -0,0 +1,175 @@ +import { h } from 'preact'; +import { css } from '@emotion/react'; +import { observer } from 'mobx-react'; + +import { defined, mergeProps } from '../../../utilities'; +import { Theme, useTheme } from '../../../providers'; +import { ComponentProps, StylingCSS } from '../../../types'; +import { RecommendationBundle, RecommendationBundleProps } from '../RecommendationBundle'; +import { Price } from '../../Atoms/Price'; +import { Button } from '../../Atoms/Button'; +import { Icon } from '../../Atoms/Icon'; +import { Image } from '../../Atoms/Image'; +import { Result } from '../../Molecules/Result'; +import { BundledCTAProps } from '../RecommendationBundle/BundleCTA'; + +const CSS = { + RecommendationBundleList: ({}: Partial) => + css({ + '.ss__recommendation-bundle__wrapper__selector__result-wrapper': { + display: 'flex', + '.ss__recommendation-bundle__wrapper__selector__result-wrapper__checkbox': { + position: 'relative', + minWidth: '20px', + }, + + '.ss__result__details': { + textAlign: 'left', + }, + }, + + '.ss__recommendation-profile-tracker': { + display: 'flex', + flexDirection: 'column', + }, + + '.ss__recommendation-bundle__wrapper': { + order: '3', + }, + + '.ss__recommendation-bundle__wrapper__cta': { + order: '2', + + '.ss__button': { + cursor: 'pointer', + border: '1px solid black', + }, + '.cta__inner_images': { + display: 'flex', + flexDirection: 'row', + }, + + '.cta__inner__image-wrapper .ss__icon': { + top: '50%', + position: 'absolute', + right: '-0.5em', + }, + + '.cta__inner__image-wrapper:last-of-type .ss__icon': { + display: 'none', + }, + + '.cta__inner__image-wrapper': { + padding: '0px 15px', + position: 'relative', + }, + }, + }), +}; + +export const RecommendationBundleList = observer((properties: RecommendationBundleListProps): JSX.Element => { + const globalTheme: Theme = useTheme(); + const defaultProps: Partial = {}; + + const props = mergeProps('recommendationBundleList', globalTheme, defaultProps, properties); + + const { treePath, styleScript, theme, style, disableStyles, ...additionalProps } = props; + + const subProps: RecommendationBundleListSubProps = { + recommendationBundle: { + // default props + className: 'ss__recommendation-bundle-list', + seedText: '', + ctaInline: false, + limit: 5, + preselectedCount: 2, + carousel: { + enabled: false, + seedInCarousel: true, + }, + ctaSlot: (props) => , + resultComponent: (props) => , + vertical: true, + separatorIcon: false, + + // inherited props + ...defined({ + disableStyles, + }), + // component theme oveRides + theme: props?.theme, + treePath, + }, + }; + + const styling: { css?: StylingCSS } = {}; + const stylingProps = { ...props, theme }; + + if (styleScript && !disableStyles) { + styling.css = [styleScript(stylingProps), style]; + } else if (!disableStyles) { + styling.css = [CSS.RecommendationBundleList(stylingProps), style]; + } else if (style) { + styling.css = [style]; + } + + return ; +}); + +export type RecommendationBundleListProps = Omit< + RecommendationBundleProps, + 'seedText' | 'vertical' | 'ctaInline' | 'ctaIcon' | 'vertical' | 'slidesPerView' +> & + ComponentProps; + +interface RecommendationBundleListSubProps { + recommendationBundle: Partial; +} + +const CTASlot = observer((props: BundledCTAProps): JSX.Element => { + const cartStore = props.cartStore; + return ( +
+
+
+ {cartStore.items.map((item: any) => { + const core = item.display.mappings.core; + return ( +
+ {core.name} + +
+ ); + })} +
+
{`${cartStore.count} item${cartStore.count != 1 ? 's' : ''} `}
+ +
+
Total Price
+
+ {cartStore.msrp > cartStore.price && ( + + + USD + + + )} + + USD + +
+
+
+
+ +
+
+ ); +}); diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/index.ts b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/index.ts new file mode 100644 index 000000000..d7a587ca7 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/index.ts @@ -0,0 +1 @@ +export * from './RecommendationBundleList'; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md new file mode 100644 index 000000000..cf42dd48f --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md @@ -0,0 +1,202 @@ +# RecommendationBundleList + +Renders a recommended bundle of products with seed, recommendations in list form, and add to cart display. + +## Sub-components +- RecommendationBundle + +### controller +The required `controller` prop specifies a reference to the RecommendationController + +```jsx +{console.log(items)}} /> +``` + +### onAddToCart +the required `onAddToCart` prop sets a the callback function for when a add to cart button is clicked. This function will be passed an array of selected item ids and their quantities. + +```jsx +{console.log(items)}} /> +``` + +### results +The `results` prop specifies a reference to the results store array to use instead of the default `controller.store.results`. Note the first result will be displayed as the `seed` product. + +```jsx +{console.log(items)}} results={controller.store.results} /> +``` + +### title +The `title` prop specifies the bundle title + +```jsx +{console.log(items)}} title={'Recommended Bundle'} /> +``` + +### resultComponent +The `resultComponent` prop allows for a custom result component to be rendered. This component will be passed the following props - + +```jsx + { + result: Product, + seed: boolean, + selected: boolean, + onProductSelect: (result:Product) => void + } +``` + +```jsx +{console.log(items)}} resultComponent={} /> +``` + + +### carousel +The `carousel` prop specifies an object of carousel settings. These settings will be merged with the default settings (listed below). All valid Carousel component props (and any non-documented SwiperJS props) can be used here. The below example uses the `prevButton`, `nextButton` and `loop` props from the Carousel: + +```jsx +type BundleCarouselProps = { + enabled: boolean; + seedInCarousel?: boolean; +} & CarouselProps + +const customCarouselProps = { + enabled: true, + loop: false, + prevButton: 'Previous', + nextButton: 'Next' +} +{console.log(items)}} carousel={ customCarouselProps } /> +``` + +### enabled +The `enabled` prop is a sub prop under the `carousel` prop. It specifies weather the bundle should render as a carousel or not. + +```jsx +{console.log(items)}} carousel={ enabled:false } /> +``` + +### hideSeed +The `hideSeed` prop specifies if the seed result should be rendered or not. + +```jsx +{console.log(items)}} hideSeed={true} /> +``` + +### seedInCarousel +The `seedInCarousel` prop is a sub prop under the `carousel` prop. It specifies if the seed product should be included in the carousel or not. + +```jsx +{console.log(items)}} carousel={ seedInCarousel:true } /> +``` + +### ctaButtonText +The `ctaButtonText` prop specifies the inner text to render in the add to cart button. + +```jsx +{console.log(items)}} ctaButtonText={'Add Bundle'} /> +``` + +### ctaButtonSuccessText +The `ctaButtonSuccessText` prop specifies text to temporarily render in the add to cart button after it is clicked. + +```jsx +{console.log(items)}} ctaButtonSuccessText={'Thanks for Shopping!'} /> +``` + +### ctaButtonSuccessTimeout +The `ctaButtonSuccessTimeout` prop specifies number of ms to show success text in add to cart button before reverting back to normal text + +```jsx +{console.log(items)}} ctaButtonSuccessTimeout={1500} /> +``` + +### ctaSlot +The `ctaSlot` prop allows for a custom add to cart cta component to be rendered. This component will be passed the following props - + +```jsx + { + cartStore: CartStore; + onclick: (e:any) => void + } +``` + +```jsx +{console.log(items)}} ctaSlot={} /> +``` + +### separatorIcon +The `separatorIcon` prop specifies the icon to render between products. Takes an object with `Icon` component props or a string. + +```jsx +{console.log(items)}} separatorIcon={'cog'} /> +``` + +### separatorIconSeedOnly +The `separatorIconSeedOnly` prop specifies if the seperator Icon should only be rendered after the seed or after every product. + +```jsx +{console.log(items)}} separatorIconSeedOnly={true} /> +``` + +### preselectedCount +The `preselectedCount` prop specifies how many products in the bundle will be preselected. This number will include the seed. Example `preselectedCount={3}` would be `seed` + 2 preselected items. If not provided, this will default to however many products are initially visible. + +```jsx +{console.log(items)}} preselectedCount={4} /> +``` + +### lazyRender +The `lazyRender` prop specifies an object of lazy rendering settings. The settings include an `enable` toggle (defaults to `true`) as well as an `offset` (default `"10%"`) to specify at what distance the component should start rendering relative to the bottom of the viewport. + +```jsx +const customLazyRenderProps = { + enabled: true, + offset: "20px" // any css margin values accepted - px, %, etc... +} + +{console.log(items)}} /> +``` + +### breakpoints +An object that modifies the responsive behavior of the carousel at various viewports. + +The object key specified the viewport for when the parameters will be applied. + +The default configuration contains the following properties, however **`any BundleRecommendation props`**, or [Swiper API parameters](https://swiperjs.com/react#swiper-props) can also be specified. + +`slidesPerView` - number of products to display per page + +`slidesPerGroup` - number of products to scroll by when next/previous button is clicked + +`spaceBetween` - spacing between each product + +```typescript +const defaultRecommendationBreakpoints = { + 0: { + carousel: { + enabled: false, + }, + limit: 2 + }, + 768: { + slidesPerView: 3, + slidesPerGroup: 3, + spaceBetween: 10, + }, + 1024: { + slidesPerView: 3, + slidesPerGroup: 3, + spaceBetween: 10, + }, + 1200: { + slidesPerView: 4, + slidesPerGroup: 4, + spaceBetween: 10, + }, +}; +``` + +```jsx +{console.log(items)}} breakpoints={defaultRecommendationBreakpoints} /> +``` + diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.stories.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.stories.tsx new file mode 100644 index 000000000..cfbd5cb1a --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.stories.tsx @@ -0,0 +1,302 @@ +import { h } from 'preact'; + +import { ArgsTable, PRIMARY_STORY, Markdown } from '@storybook/blocks'; + +import { RecommendationBundleVertical, RecommendationBundleVerticalProps } from './RecommendationBundleVertical'; +import { componentArgs, highlightedCode } from '../../../utilities'; +import { Snapify } from '../../../utilities/snapify'; + +import Readme from './readme.md'; +import type { RecommendationController } from '@searchspring/snap-controller'; +import type { Product } from '@searchspring/snap-store-mobx'; +import type { Next } from '@searchspring/snap-event-manager'; +import { iconPaths } from '../../Atoms/Icon'; +import type { RecommendationControllerConfig } from '@searchspring/snap-controller'; + +export default { + title: 'Templates/RecommendationBundleVertical', + component: RecommendationBundleVertical, + tags: ['autodocs'], + parameters: { + docs: { + page: () => ( +
+ + {Readme} + + +
+ ), + }, + }, + decorators: [ + (Story: any) => ( +
+ +
+ ), + ], + argTypes: { + controller: { + description: 'Controller reference', + type: { required: true }, + table: { + type: { + summary: 'Controller', + }, + }, + control: { type: 'none' }, + }, + results: { + description: 'Results store reference, overrides controller.store.results', + type: { required: false }, + table: { + type: { + summary: 'Results store object', + }, + }, + control: { type: 'none' }, + }, + resultComponent: { + description: 'Slot for custom result component', + table: { + type: { + summary: 'component', + }, + }, + }, + title: { + description: 'recommendation title', + table: { + type: { + summary: 'string | JSX Element', + }, + defaultValue: { summary: '' }, + }, + control: { type: 'text' }, + }, + onAddToCart: { + description: 'onClick event handler for add bundle to cart button in CTA', + type: { required: true }, + table: { + type: { + summary: 'function', + }, + }, + action: 'onAddToCart', + }, + limit: { + description: 'limit the number of results rendered', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: 3, + }, + }, + control: { type: 'number' }, + }, + carousel: { + description: 'Carousel settings object', + defaultValue: { + enabled: true, + loop: false, + }, + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Carousel settings object' }, + }, + control: { type: 'object' }, + }, + preselectedCount: { + description: 'Number of results to have selected by default. (seed included)', + table: { + type: { + summary: 'number', + }, + }, + control: { type: 'number' }, + }, + hideCheckboxes: { + defaultValue: false, + description: 'Hide/show bundle checkboxes in results', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: false }, + }, + control: { type: 'boolean' }, + }, + seedText: { + description: 'Text to render in seed product badge', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Seed Product' }, + }, + control: { type: 'text' }, + }, + hideSeed: { + description: 'Hide/show seed result', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: true }, + }, + control: { type: 'boolean' }, + }, + separatorIconSeedOnly: { + description: 'boolean to only have seperator Icon for the seed product', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: true }, + }, + control: { type: 'boolean' }, + }, + separatorIcon: { + defaultValue: 'plus', + description: 'Icon to render between results', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'plus' }, + }, + control: { + type: 'select', + options: [...Object.keys(iconPaths)], + }, + }, + ctaButtonText: { + description: 'text to render in add to cart button', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Add All To Cart' }, + }, + control: { type: 'text' }, + }, + ctaIcon: { + desciption: 'The `ctaIcon` prop specifies the icon to render in the CTA. Takes an object with `Icon` component props or a string.', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'bag' }, + }, + control: { + type: 'select', + options: [...Object.keys(iconPaths)], + }, + }, + ctaButtonSuccessText: { + description: 'text to temporarily render in the add to cart button after it is clicked', + table: { + type: { + summary: 'string', + }, + defaultValue: { summary: 'Bundle Added!' }, + }, + control: { type: 'text' }, + }, + ctaButtonSuccessTimeout: { + description: 'Number of ms to show success text in add to cart button before reverting back to normal text', + defaultValue: 2000, + table: { + type: { + summary: 'number', + }, + }, + control: { type: 'number' }, + }, + ctaSlot: { + description: 'Slot for custom add to cart component', + table: { + type: { + summary: 'component', + }, + }, + }, + lazyRender: { + description: 'Lazy render settings object', + defaultValue: { + enabled: true, + offset: '10%', + }, + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Lazy render settings object' }, + }, + control: { type: 'object' }, + }, + breakpoints: { + defaultValue: undefined, + description: 'Recommendation title', + table: { + type: { + summary: 'object', + }, + defaultValue: { summary: 'Breakpoint object' }, + }, + control: { type: 'object' }, + }, + ...componentArgs, + }, +}; + +const config: RecommendationControllerConfig = { + id: 'RecommendationBundle', + tag: 'bundle', + globals: { + siteId: '8uyt2m', + products: ['C-AD-W1-1869P'], + }, +}; + +const snapInstance = Snapify.recommendation(config); + +export const Default = ( + props: RecommendationBundleVerticalProps, + { loaded: { controller } }: { loaded: { controller: RecommendationController } } +) => { + return ; +}; + +Default.loaders = [ + async () => { + snapInstance.on('afterStore', async ({ controller }: { controller: RecommendationController }, next: Next) => { + controller.store.results.forEach((result: Product) => (result.mappings.core!.url = 'javascript:void(0);')); + await next(); + }); + await snapInstance.search(); + return { + controller: snapInstance, + }; + }, +]; + +Default.args = { + limit: 3, +}; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx new file mode 100644 index 000000000..765d50a30 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx @@ -0,0 +1,68 @@ +import { h } from 'preact'; +import { css } from '@emotion/react'; +import { observer } from 'mobx-react'; + +import { defined, mergeProps } from '../../../utilities'; +import { Theme, useTheme } from '../../../providers'; +import { ComponentProps, StylingCSS } from '../../../types'; +import { RecommendationBundle, RecommendationBundleProps } from '../RecommendationBundle'; + +const CSS = { + RecommendationBundleVertical: ({}: Partial) => + css({ + '.ss__recommendation-bundle__wrapper': { + flexDirection: 'column', + }, + '.ss__recommendation-bundle__wrapper__cta': { + textAlign: 'center', + }, + }), +}; + +export const RecommendationBundleVertical = observer((properties: RecommendationBundleVerticalProps): JSX.Element => { + const globalTheme: Theme = useTheme(); + const defaultProps: Partial = {}; + + const props = mergeProps('recommendationBundleVertical', globalTheme, defaultProps, properties); + + const { treePath, styleScript, theme, style, disableStyles, ...additionalProps } = props; + + const subProps: RecommendationBundleVerticalSubProps = { + recommendationBundle: { + // default props + className: 'ss__recommendation-bundle-vertical', + ctaInline: false, + carousel: { + enabled: false, + }, + separatorIcon: false, + + // inherited props + ...defined({ + disableStyles, + }), + // component theme overrides + theme: props?.theme, + treePath, + }, + }; + + const styling: { css?: StylingCSS } = {}; + const stylingProps = { ...props, theme }; + + if (styleScript && !disableStyles) { + styling.css = [styleScript(stylingProps), style]; + } else if (!disableStyles) { + styling.css = [CSS.RecommendationBundleVertical(stylingProps), style]; + } else if (style) { + styling.css = [style]; + } + + return ; +}); + +export type RecommendationBundleVerticalProps = Omit & ComponentProps; + +interface RecommendationBundleVerticalSubProps { + recommendationBundle: Partial; +} diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/index.ts b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/index.ts new file mode 100644 index 000000000..0ce46758e --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/index.ts @@ -0,0 +1 @@ +export * from './RecommendationBundleVertical'; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/readme.md b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/readme.md new file mode 100644 index 000000000..ddc3e0b7c --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/readme.md @@ -0,0 +1,262 @@ +# RecommendationBundleVertical + +Vertically renders a recommended bundle of products with seed, recommendations and add to cart display. + +## Sub-components +- RecommendationBundle + +## Usage + +Additional [Swiper Component Props](https://swiperjs.com/react#swiper-props) can be specified, but may need to be camelCased where appropriate. +Additional [Swiper Modules](https://swiperjs.com/swiper-api#modules) can be provided via the `modules` prop; these may need additional props and or stylesheets. + +### controller +The required `controller` prop specifies a reference to the RecommendationController + +```jsx +{console.log(items)}} /> +``` + +### onAddToCart +the required `onAddToCart` prop sets a the callback function for when a add to cart button is clicked. This function will be passed an array of selected item ids and their quantities. + +```jsx +{console.log(items)}} /> +``` + +### results +The `results` prop specifies a reference to the results store array to use instead of the default `controller.store.results`. Note the first result will be displayed as the `seed` product. + +```jsx +{console.log(items)}} results={controller.store.results} /> +``` + +### carousel +The `carousel` prop specifies an object of carousel settings. These settings will be merged with the default settings (listed below). All valid Carousel component props (and any non-documented SwiperJS props) can be used here. The below example uses the `prevButton`, `nextButton` and `loop` props from the Carousel: + +```jsx +type BundleCarouselProps = { + enabled: boolean; + seedInCarousel?: boolean; +} & CarouselProps + +const customCarouselProps = { + enabled: false +} + +{console.log(items)}} carousel={ customCarouselProps } /> +``` + +### enabled +The `enabled` prop is a sub prop under the `carousel` prop. It specifies weather the bundle should render as a carousel or not. + +```jsx +{console.log(items)}} carousel={ enabled:true } /> +``` + +### hideSeed +The `hideSeed` prop specifies if the seed result should be rendered or not. + +```jsx +{console.log(items)}} hideSeed={true} /> +``` + +### seedInCarousel +The `seedInCarousel` prop is a sub prop under the `carousel` prop. It specifies if the seed product should be included in the carousel or not. + +```jsx +{console.log(items)}} carousel={ seedInCarousel:true } /> +``` + +### pagination +The `pagination` prop is a sub prop under the `carousel` prop. It specifies if the carousel should display pagination dots. + +```jsx +{console.log(items)}} carousel={ pagination:true } /> +``` + +### hideButtons +The `hideButtons` is a sub prop under the `carousel` prop. It specifies if the carousel should hide prev/next buttons. + +```jsx +{console.log(items)}} carousel={ hideButtons:true }> +``` + +### prevButton +The `prevButton` prop is a sub prop under the `carousel` prop. It specifies the previous button element of the carousel. This can be a string or JSX element. + +```jsx +{console.log(items)}} carousel={ prevButton: '<' } /> +``` + +### nextButton +The `nextButton` prop is a sub prop under the `carousel` prop. It specifies the next button element of the carousel. This can be a string or JSX element. + +```jsx +{console.log(items)}} carousel={ nextButton: '>' } /> +``` + +### title +The `title` prop specifies the carousel title + +```jsx +{console.log(items)}} title={'Recommended Bundle'} /> +``` + +### resultComponent +The `resultComponent` prop allows for a custom result component to be rendered. This component will be passed the following props - + +```jsx + { + result: Product, + seed: boolean, + selected: boolean, + onProductSelect: (result:Product) => void + } +``` + +```jsx +{console.log(items)}} resultComponent={} /> +``` + +### ctaButtonText +The `ctaButtonText` prop specifies the inner text to render in the add to cart button. + +```jsx +{console.log(items)}} ctaButtonText={'Add Bundle'} /> +``` + +### ctaButtonSuccessText +The `ctaButtonSuccessText` prop specifies text to temporarily render in the add to cart button after it is clicked. + +```jsx +{console.log(items)}} ctaButtonSuccessText={'Thanks for Shopping!'} /> +``` + +### ctaButtonSuccessTimeout +The `ctaButtonSuccessTimeout` prop specifies number of ms to show success text in add to cart button before reverting back to normal text + +```jsx +{console.log(items)}} ctaButtonSuccessTimeout={1500} /> +``` + +### ctaIcon +The `ctaIcon` prop specifies the icon to render in the CTA. Takes an object with `Icon` component props or a string. + +```jsx +{console.log(items)}} ctaIcon={'bag'} /> +``` + +### ctaSlot +The `ctaSlot` prop allows for a custom add to cart cta component to be rendered. This component will be passed the following props - + +```jsx + { + cartStore: CartStore; + onclick: (e:any) => void + } +``` + +```jsx +{console.log(items)}} ctaSlot={} /> +``` + +### preselectedCount +The `preselectedCount` prop specifies how many products in the bundle will be preselected. This number will include the seed. Example `preselectedCount={3}` would be `seed` + 2 preselected items. If not provided, this will default to however many products are initially visible. + +```jsx +{console.log(items)}} preselectedCount={4} /> +``` + +### seedText +The `seedText` prop specifies text to be rendered as a badge in the seed product. + +```jsx +{console.log(items)}} seedText={"Main Product"} /> +``` + +### separatorIcon +The `separatorIcon` prop specifies the icon to render between products. Takes an object with `Icon` component props or a string. + +```jsx +{console.log(items)}} separatorIcon={'cog'} /> +``` + +### separatorIconSeedOnly +The `separatorIconSeedOnly` prop specifies if the seperator Icon should only be rendered after the seed or after every product. + +```jsx +{console.log(items)}} separatorIconSeedOnly={true} /> +``` + + +### hideCheckboxes +The `hideCheckboxes` prop specifies if the bundle checkboxes should be rendered. + +```jsx +{console.log(items)}} hideCheckboxes={true} /> +``` + +### modules +The `modules` prop accepts additional [Swiper Modules](https://swiperjs.com/swiper-api#modules) - these may need additional props and or stylesheets to function. We include `Navigation` and `Pagination` modules by default. + +```jsx +import { Scrollbar } from 'swiper'; +{console.log(items)}} modules={[Scrollbar]} scrollbar={{ draggable: true }} /> +``` + +### lazyRender +The `lazyRender` prop specifies an object of lazy rendering settings. The settings include an `enable` toggle (defaults to `true`) as well as an `offset` (default `"10%"`) to specify at what distance the component should start rendering relative to the bottom of the viewport. + +```jsx +const customLazyRenderProps = { + enabled: true, + offset: "20px" // any css margin values accepted - px, %, etc... +} + +{console.log(items)}} /> +``` + +### breakpoints +An object that modifies the responsive behavior of the carousel at various viewports. + +The object key specified the viewport for when the parameters will be applied. + +The default configuration contains the following properties, however **`any BundleRecommendation props`**, or [Swiper API parameters](https://swiperjs.com/react#swiper-props) can also be specified. + +`slidesPerView` - number of products to display per page + +`slidesPerGroup` - number of products to scroll by when next/previous button is clicked + +`spaceBetween` - spacing between each product + +```typescript +const defaultRecommendationBreakpoints = { + 0: { + carousel: { + enabled: false, + }, + limit: 2 + }, + 768: { + slidesPerView: 3, + slidesPerGroup: 3, + spaceBetween: 10, + }, + 1024: { + slidesPerView: 3, + slidesPerGroup: 3, + spaceBetween: 10, + }, + 1200: { + slidesPerView: 4, + slidesPerGroup: 4, + spaceBetween: 10, + }, +}; +``` + +```jsx +{console.log(items)}} breakpoints={defaultRecommendationBreakpoints} /> +``` + diff --git a/packages/snap-preact/components/src/utilities/cloneWithProps.tsx b/packages/snap-preact/components/src/utilities/cloneWithProps.tsx index 710f1ebe1..7726e7674 100644 --- a/packages/snap-preact/components/src/utilities/cloneWithProps.tsx +++ b/packages/snap-preact/components/src/utilities/cloneWithProps.tsx @@ -3,6 +3,8 @@ import { h, cloneElement } from 'preact'; export const cloneWithProps = (input: any, props?: any): any => { if (!input) { return; + } else if (typeof input == 'function') { + return input(props); } else if (typeof input == 'string' || typeof input == 'number' || typeof input == 'boolean') { return input; } else if (Array.isArray(input)) {