From f90a1e1b24cc35d2206bb07fdc65114a352b1e60 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 26 Sep 2024 16:08:07 -0700 Subject: [PATCH 1/3] feat(preact-components/recommendationbundle): adding new components for bundle variant components --- .../RecommendationBundle.stories.tsx | 1 + .../RecommendationBundleEasyAdd.stories.tsx | 238 ++++++++++++++ .../RecommendationBundleEasyAdd.tsx | 74 +++++ .../RecommendationBundleEasyAdd/index.ts | 1 + .../RecommendationBundleEasyAdd/readme.md | 170 ++++++++++ .../RecommendationBundleList.stories.tsx | 209 ++++++++++++ .../RecommendationBundleList.tsx | 195 +++++++++++ .../RecommendationBundleList/index.ts | 1 + .../RecommendationBundleList/readme.md | 148 +++++++++ .../RecommendationBundleVertical.stories.tsx | 302 ++++++++++++++++++ .../RecommendationBundleVertical.tsx | 68 ++++ .../RecommendationBundleVertical/index.ts | 1 + .../RecommendationBundleVertical/readme.md | 262 +++++++++++++++ 13 files changed, 1670 insertions(+) create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.stories.tsx create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/index.ts create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/readme.md create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.stories.tsx create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleList/index.ts create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.stories.tsx create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/index.ts create mode 100644 packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/readme.md 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/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..07d76f785 --- /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 } = props; + + const subProps: RecommendationBundleEasyAddSubProps = { + recommendationBundle: { + // default props + className: 'ss__recommendation-easy-add__recommendation-bundle', + 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..0c0ec0eee --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.stories.tsx @@ -0,0 +1,209 @@ +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'; + +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' }, + }, + 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..e646a9dbe --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx @@ -0,0 +1,195 @@ +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'; + +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', + }, + }, + + '.ss__recommendation-bundle': { + '.ss__recommendation-bundle__wrapper__cta': { + '.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 } = props; + + const subProps: RecommendationBundleListSubProps = { + recommendationBundle: { + // default props + className: 'ss__recommendation-bundle-list__recommendation-bundle', + seedText: '', + ctaInline: false, + limit: 5, + preselectedCount: 2, + carousel: { + enabled: false, + seedInCarousel: true, + }, + ctaSlot: , + // @ts-ignore - TODO typing for this seems wrong. + resultComponent: , + 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, + | 'carousel' + | 'seedText' + | 'hideSeed' + | 'vertical' + | 'separatorIconSeedOnly' + | 'separatorIcon' + | 'ctaInline' + | 'ctaIcon' + | 'vertical' + | 'carousel' + | 'slidesPerView' +> & + ComponentProps; + +interface RecommendationBundleListSubProps { + recommendationBundle: Partial; +} + +const CTASlot = observer((props: any) => { + 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..b4c62f5a0 --- /dev/null +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md @@ -0,0 +1,148 @@ +# 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={} /> +``` + +### 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={} /> +``` + +### 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..15c6ceb9e --- /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 } = props; + + const subProps: RecommendationBundleVerticalSubProps = { + recommendationBundle: { + // default props + className: 'ss__recommendation-bundle-vertical__recommendation-bundle', + 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} /> +``` + From 3d96ce633eb44f991d1eda17600c1bb2ea5152ae Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 27 Sep 2024 09:27:17 -0700 Subject: [PATCH 2/3] refactor(preact-components/bundlevariants): fix prop spreading on bundle variants issue --- .../RecommendationBundleEasyAdd.tsx | 2 +- .../RecommendationBundleList.stories.tsx | 50 +++++++++++++++++ .../RecommendationBundleList.tsx | 14 +---- .../RecommendationBundleList/readme.md | 54 +++++++++++++++++++ .../RecommendationBundleVertical.tsx | 2 +- 5 files changed, 108 insertions(+), 14 deletions(-) diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx index 07d76f785..ac8507e6b 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx @@ -60,7 +60,7 @@ export const RecommendationBundleEasyAdd = observer((properties: RecommendationB styling.css = [style]; } - return ; + return ; }); export type RecommendationBundleEasyAddProps = Omit< 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 index 0c0ec0eee..0189e9747 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.stories.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.stories.tsx @@ -11,6 +11,7 @@ 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', @@ -104,6 +105,55 @@ export default { }, 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: { diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx index e646a9dbe..03a0afb1b 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx @@ -120,24 +120,14 @@ export const RecommendationBundleList = observer((properties: RecommendationBund return (
- +
); }); export type RecommendationBundleListProps = Omit< RecommendationBundleProps, - | 'carousel' - | 'seedText' - | 'hideSeed' - | 'vertical' - | 'separatorIconSeedOnly' - | 'separatorIcon' - | 'ctaInline' - | 'ctaIcon' - | 'vertical' - | 'carousel' - | 'slidesPerView' + 'seedText' | 'vertical' | 'ctaInline' | 'ctaIcon' | 'vertical' | 'slidesPerView' > & ComponentProps; diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md index b4c62f5a0..cf42dd48f 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/readme.md @@ -49,6 +49,46 @@ The `resultComponent` prop allows for a custom result component to be rendered. {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. @@ -84,6 +124,20 @@ The `ctaSlot` prop allows for a custom add to cart cta component to be rendered. {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. diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx index 15c6ceb9e..cb77e7f92 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx @@ -58,7 +58,7 @@ export const RecommendationBundleVertical = observer((properties: Recommendation styling.css = [style]; } - return ; + return ; }); export type RecommendationBundleVerticalProps = Omit & ComponentProps; From 85cb7f00bea051f2bd3031e59bfe7e9a5c8e5404 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 30 Sep 2024 16:13:48 -0700 Subject: [PATCH 3/3] refactor(preact-components/bundle-variants): some tweaks to bundle variant components --- .../RecommendationBundle/BundleCTA.tsx | 4 +- .../RecommendationBundle.tsx | 11 +++- .../RecommendationBundleEasyAdd.tsx | 8 +-- .../RecommendationBundleList.tsx | 62 ++++++++----------- .../RecommendationBundleVertical.tsx | 8 +-- .../src/utilities/cloneWithProps.tsx | 2 + 6 files changed, 46 insertions(+), 49 deletions(-) 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.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.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx index ac8507e6b..e23b5fe24 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleEasyAdd/RecommendationBundleEasyAdd.tsx @@ -20,14 +20,14 @@ export const RecommendationBundleEasyAdd = observer((properties: RecommendationB const globalTheme: Theme = useTheme(); const defaultProps: Partial = {}; - const props = mergeProps('RecommendationBundleEasyAdd', globalTheme, defaultProps, properties); + const props = mergeProps('recommendationBundleEasyAdd', globalTheme, defaultProps, properties); - const { treePath, styleScript, theme, style, disableStyles } = props; + const { treePath, styleScript, theme, style, disableStyles, ...additionalProps } = props; const subProps: RecommendationBundleEasyAddSubProps = { recommendationBundle: { // default props - className: 'ss__recommendation-easy-add__recommendation-bundle', + className: 'ss__recommendation-bundle-easy-add', hideCheckboxes: true, seedText: '', ctaButtonText: 'Add Both', @@ -60,7 +60,7 @@ export const RecommendationBundleEasyAdd = observer((properties: RecommendationB styling.css = [style]; } - return ; + return ; }); export type RecommendationBundleEasyAddProps = Omit< diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx index 03a0afb1b..d94f180c4 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleList/RecommendationBundleList.tsx @@ -11,6 +11,7 @@ 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) => @@ -43,29 +44,24 @@ const CSS = { 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', + }, - '.ss__recommendation-bundle': { - '.ss__recommendation-bundle__wrapper__cta': { - '.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', - }, + '.cta__inner__image-wrapper:last-of-type .ss__icon': { + display: 'none', + }, + + '.cta__inner__image-wrapper': { + padding: '0px 15px', + position: 'relative', }, }, }), @@ -75,14 +71,14 @@ export const RecommendationBundleList = observer((properties: RecommendationBund const globalTheme: Theme = useTheme(); const defaultProps: Partial = {}; - const props = mergeProps('RecommendationBundleList', globalTheme, defaultProps, properties); + const props = mergeProps('recommendationBundleList', globalTheme, defaultProps, properties); - const { treePath, styleScript, theme, style, disableStyles } = props; + const { treePath, styleScript, theme, style, disableStyles, ...additionalProps } = props; const subProps: RecommendationBundleListSubProps = { recommendationBundle: { // default props - className: 'ss__recommendation-bundle-list__recommendation-bundle', + className: 'ss__recommendation-bundle-list', seedText: '', ctaInline: false, limit: 5, @@ -91,9 +87,8 @@ export const RecommendationBundleList = observer((properties: RecommendationBund enabled: false, seedInCarousel: true, }, - ctaSlot: , - // @ts-ignore - TODO typing for this seems wrong. - resultComponent: , + ctaSlot: (props) => , + resultComponent: (props) => , vertical: true, separatorIcon: false, @@ -118,11 +113,7 @@ export const RecommendationBundleList = observer((properties: RecommendationBund styling.css = [style]; } - return ( -
- -
- ); + return ; }); export type RecommendationBundleListProps = Omit< @@ -135,9 +126,8 @@ interface RecommendationBundleListSubProps { recommendationBundle: Partial; } -const CTASlot = observer((props: any) => { +const CTASlot = observer((props: BundledCTAProps): JSX.Element => { const cartStore = props.cartStore; - return (
@@ -175,7 +165,7 @@ const CTASlot = observer((props: any) => { disabled={cartStore.items.length == 0} disableStyles className={`cta__add-button ${props.addedToCart ? 'cta__add-button--thanks' : ''}`} - onClick={() => props.onAddToCart()} + onClick={(e) => props.onAddToCart(e)} > {props.addedToCart ? props.ctaButtonSuccessText : props.ctaButtonText} diff --git a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx index cb77e7f92..765d50a30 100644 --- a/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx +++ b/packages/snap-preact/components/src/components/Templates/RecommendationBundleVertical/RecommendationBundleVertical.tsx @@ -23,14 +23,14 @@ export const RecommendationBundleVertical = observer((properties: Recommendation const globalTheme: Theme = useTheme(); const defaultProps: Partial = {}; - const props = mergeProps('RecommendationBundleVertical', globalTheme, defaultProps, properties); + const props = mergeProps('recommendationBundleVertical', globalTheme, defaultProps, properties); - const { treePath, styleScript, theme, style, disableStyles } = props; + const { treePath, styleScript, theme, style, disableStyles, ...additionalProps } = props; const subProps: RecommendationBundleVerticalSubProps = { recommendationBundle: { // default props - className: 'ss__recommendation-bundle-vertical__recommendation-bundle', + className: 'ss__recommendation-bundle-vertical', ctaInline: false, carousel: { enabled: false, @@ -58,7 +58,7 @@ export const RecommendationBundleVertical = observer((properties: Recommendation styling.css = [style]; } - return ; + return ; }); export type RecommendationBundleVerticalProps = Omit & ComponentProps; 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)) {