diff --git a/packages/snap-client/src/Client/Client.ts b/packages/snap-client/src/Client/Client.ts index cf1dd0aea..94fb90379 100644 --- a/packages/snap-client/src/Client/Client.ts +++ b/packages/snap-client/src/Client/Client.ts @@ -188,13 +188,15 @@ export class Client { siteId: params.siteId || this.globals.siteId, }; - const [profile, recommendations] = await Promise.all([ + const [meta, profile, recommendations] = await Promise.all([ + this.meta({ siteId: params.siteId || '' }), this.requesters.recommend.getProfile(profileParams), this.requesters.recommend.batchRecommendations(recommendParams), ]); return { ...profile, + meta, results: recommendations[0].results, }; } diff --git a/packages/snap-client/src/Client/apis/Hybrid.ts b/packages/snap-client/src/Client/apis/Hybrid.ts index fcd649c0c..97adf0743 100644 --- a/packages/snap-client/src/Client/apis/Hybrid.ts +++ b/packages/snap-client/src/Client/apis/Hybrid.ts @@ -149,7 +149,7 @@ export class HybridAPI extends API { }, tags: { 'on-sale': { - location: 'left-middle', + location: 'left-middle-upper', component: 'BadgePill', parameters: { color: '#0000FF', diff --git a/packages/snap-client/src/types.ts b/packages/snap-client/src/types.ts index 432312f26..b476f720c 100644 --- a/packages/snap-client/src/types.ts +++ b/packages/snap-client/src/types.ts @@ -1,5 +1,11 @@ import { AppMode } from '@searchspring/snap-toolbox'; -import type { MetaRequestModel, SearchResponseModelResult, SearchRequestModel, AutocompleteRequestModel } from '@searchspring/snapi-types'; +import type { + MetaRequestModel, + SearchResponseModelResult, + SearchRequestModel, + AutocompleteRequestModel, + MetaResponseModel, +} from '@searchspring/snapi-types'; export type HTTPHeaders = { [key: string]: string }; @@ -192,4 +198,4 @@ type RecommendationRequestValueFilterModel = { value: string | number; }; -export type RecommendCombinedResponseModel = ProfileResponseModel & { results: SearchResponseModelResult[] }; +export type RecommendCombinedResponseModel = ProfileResponseModel & { results: SearchResponseModelResult[] } & { meta: MetaResponseModel }; diff --git a/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.stories.tsx b/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.stories.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.stories.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.test.tsx b/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.test.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.test.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.tsx b/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.tsx new file mode 100644 index 000000000..f8dd441bf --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgeImage/BadgeImage.tsx @@ -0,0 +1,61 @@ +/** @jsx jsx */ +import { Fragment, h } from 'preact'; + +import { jsx, css } from '@emotion/react'; +import classnames from 'classnames'; +import { observer } from 'mobx-react-lite'; + +import { Theme, useTheme, CacheProvider } from '../../../providers'; +import { ComponentProps, StylingCSS } from '../../../types'; +import type { ResultBadge, OverlayResultBadge } from '@searchspring/snap-store-mobx'; + +const CSS = { + BadgeImage: ({}: BadgeImageProps) => { + return css({}); + }, +}; + +export const BadgeImage = observer((properties: BadgeImageProps): JSX.Element => { + const globalTheme: Theme = useTheme(); + + const props: BadgeImageProps = { + // default props + // global theme + ...globalTheme?.components?.badgeImage, + // props + ...properties, + ...properties.theme?.components?.badgeImage, + }; + const { badge, disableStyles, className, style } = props; + + const styling: { css?: StylingCSS } = {}; + + if (!disableStyles) { + styling.css = [CSS.BadgeImage(props), style]; + } else if (style) { + styling.css = [style]; + } + + return badge?.parameters?.url ? ( + + {badge.label} + + ) : ( + + ); +}); + +export interface BadgeImageProps extends ComponentProps { + badge: ResultBadge | OverlayResultBadge; +} diff --git a/packages/snap-preact-components/src/components/Atoms/BadgeImage/index.ts b/packages/snap-preact-components/src/components/Atoms/BadgeImage/index.ts new file mode 100644 index 000000000..bd28ae6ca --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgeImage/index.ts @@ -0,0 +1 @@ +export * from './BadgeImage'; diff --git a/packages/snap-preact-components/src/components/Atoms/BadgeImage/readme.md b/packages/snap-preact-components/src/components/Atoms/BadgeImage/readme.md new file mode 100644 index 000000000..8ecd13918 --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgeImage/readme.md @@ -0,0 +1 @@ +# BadgeImage // TODO diff --git a/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.stories.tsx b/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.stories.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.stories.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.test.tsx b/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.test.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.test.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.tsx b/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.tsx new file mode 100644 index 000000000..498d7eae2 --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgePill/BadgePill.tsx @@ -0,0 +1,68 @@ +/** @jsx jsx */ +import { Fragment, h } from 'preact'; + +import { jsx, css } from '@emotion/react'; +import classnames from 'classnames'; +import { observer } from 'mobx-react-lite'; + +import { Theme, useTheme, CacheProvider } from '../../../providers'; +import { ComponentProps, StylingCSS } from '../../../types'; +import type { ResultBadge, OverlayResultBadge } from '@searchspring/snap-store-mobx'; + +const CSS = { + BadgePill: (props: BadgePillProps) => { + const { parameters } = props.badge; + const { color, colorText } = parameters; + return css({ + padding: '0.2em 0.5em', + + background: color || 'rgba(255, 255, 255, 0.5)', + color: colorText || '#000000', + }); + }, +}; + +export const BadgePill = observer((properties: BadgePillProps): JSX.Element => { + const globalTheme: Theme = useTheme(); + + const props: BadgePillProps = { + // default props + // global theme + ...globalTheme?.components?.badgePill, + // props + ...properties, + ...properties.theme?.components?.badgePill, + }; + const { badge, disableStyles, className, style } = props; + + const styling: { css?: StylingCSS } = {}; + + if (!disableStyles) { + styling.css = [CSS.BadgePill(props), style]; + } else if (style) { + styling.css = [style]; + } + + return badge ? ( + +
+ {badge.label} +
+
+ ) : ( + + ); +}); + +export interface BadgePillProps extends ComponentProps { + badge: ResultBadge | OverlayResultBadge; +} diff --git a/packages/snap-preact-components/src/components/Atoms/BadgePill/index.ts b/packages/snap-preact-components/src/components/Atoms/BadgePill/index.ts new file mode 100644 index 000000000..146bdbb1d --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgePill/index.ts @@ -0,0 +1 @@ +export * from './BadgePill'; diff --git a/packages/snap-preact-components/src/components/Atoms/BadgePill/readme.md b/packages/snap-preact-components/src/components/Atoms/BadgePill/readme.md new file mode 100644 index 000000000..ee0ae2407 --- /dev/null +++ b/packages/snap-preact-components/src/components/Atoms/BadgePill/readme.md @@ -0,0 +1,2 @@ +# BadgePill // TODO + diff --git a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.stories.tsx b/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.stories.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.test.tsx b/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.test.tsx deleted file mode 100644 index 211741201..000000000 --- a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.test.tsx +++ /dev/null @@ -1,5 +0,0 @@ -describe('Result Component', () => { - it('todo', () => { - expect(1).toBe(1); - }); -}); diff --git a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.tsx b/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.tsx deleted file mode 100644 index 64de2e0c3..000000000 --- a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/SelfServeBadge.tsx +++ /dev/null @@ -1,257 +0,0 @@ -/** @jsx jsx */ -import { ComponentChildren, Fragment, h } from 'preact'; - -import { jsx, css } from '@emotion/react'; -import type { CSSObject } from '@emotion/serialize'; -import classnames from 'classnames'; -import { observer } from 'mobx-react-lite'; - -import { Theme, useTheme, CacheProvider } from '../../../providers'; -import { ComponentProps, StylingCSS } from '../../../types'; - -import type { AbstractController, SearchController } from '@searchspring/snap-controller'; -import type { Product } from '@searchspring/snap-store-mobx'; - -const positioningStyles = ({ type, overlayLocation, location, overlayLocationOptions }: SelfServeBadgeProps): CSSObject => { - let styles: CSSObject = { - [overlayLocation]: 0, // left or right - }; - - if (type === 'callout') { - styles = { ...styles, display: 'inline-block' }; - } else if (type === 'overlay') { - styles = { ...styles, position: 'absolute' }; - - const index = overlayLocationOptions.findIndex((option: any) => option.name === location); - if (index === 0) { - // first option, assume top - styles = { ...styles, top: 0 }; - } else if (index === overlayLocationOptions.length - 1) { - // last in list, assume bottom - styles = { ...styles, bottom: 0 }; - } else if (index > 0 && index < overlayLocationOptions.length - 1) { - // middle options, calculate percentage based on position in list - const percentage = (index / (overlayLocationOptions.length - 1)) * 100; - styles = { ...styles, top: `${percentage}%`, transform: `translateY(-${percentage}%)`, margin: 'auto' }; - } - } - - return styles; -}; -const CSS = { - selfServeBadge: ({ type }: SelfServeBadgeProps) => - css({ - position: type === 'overlay' ? 'relative' : undefined, - }), - BadgePill: (props: SelfServeBadgeProps) => { - const { parameters } = props; - const { color, colorText } = parameters; - return css({ - ...positioningStyles(props), - - padding: '0.2em 0.5em', - - background: color || 'rgba(255, 255, 255, 0.5)', - color: colorText || '#000000', - }); - }, - BadgeImage: (props: SelfServeBadgeProps) => { - const { parameters } = props; - const { color, colorText } = parameters; - return css({ - ...positioningStyles(props), - - background: color || 'rgba(255, 255, 255, 0.5)', - color: colorText || '#000000', - }); - }, -}; - -export const SelfServeBadge = observer((properties: SelfServeBadgeProps): JSX.Element => { - const globalTheme: Theme = useTheme(); - - const props: SelfServeBadgeProps = { - // default props - callout: properties.children ? '' : 'callout', - type: properties.children ? 'overlay' : 'callout', - // global theme - ...globalTheme?.components?.badge, - // props - ...properties, - ...properties.theme?.components?.badge, - }; - const { result, callout, type, children, controller, disableStyles, className, style } = props; - - const styling: { css?: StylingCSS } = {}; - - // { - // tag: 'on-sale', - // label: '30% Off', - // component: '', - // location: 'left', - // parameters: { - // color: '#0000FF', - // colorText: '#FFFFFF', - // url: '', - // }, - // } - - // locations: { - // overlay: { - // [position: string]: { - // name: string; - // label: string; - // description: string; - // }[]; - // }; - // callouts: { - // name: string; - // label: string; - // description: string; - // }[]; - // }; - - if (!disableStyles) { - styling.css = [CSS.selfServeBadge(props), style]; - } else if (style) { - styling.css = [style]; - } - - const resultBadges = result.badges; - const locations = (controller?.store as any)?.meta?.badges?.locations; - if (!resultBadges?.length || !locations) { - return {children}; - } - - const callouts = locations?.callouts; - const overlay = locations?.overlay; - - const calloutBadge = resultBadges.find((badge: any) => { - return callouts?.some((callout: any) => callout.name === badge.location) && badge.location === callout; - }); - - const overlayBadges = - resultBadges - ?.map((badge: any) => { - // add overlayLocation property to badge to be used in styling - const isRightOverlay = overlay.right.some((rightOverlays: any) => rightOverlays.name === badge.location); - const isLeftOverlay = overlay.left.some((leftOverlays: any) => leftOverlays.name === badge.location); - const overlayLocation = isRightOverlay ? 'right' : isLeftOverlay ? 'left' : ''; - - return { - ...badge, - overlayLocation, - overlayLocationOptions: overlay[overlayLocation], - }; - }) - .filter((badge: any) => { - // filter out badges that are not overlay badges - return badge.overlayLocation; - }) || []; - - if (calloutBadge && type === 'callout') { - return ( - -
- {(() => { - switch (calloutBadge.component) { - case 'BadgePill': - return ; - case 'BadgeImage': - return ; - default: - return; - } - })()} -
-
- ); - } - - if (overlayBadges.length && type === 'overlay') { - console.log('overlayBadges', overlayBadges); - return ( - -
- {children} - {overlayBadges.map((badge: any) => { - switch (badge.component) { - case 'BadgePill': - return ; - case 'BadgeImage': - return ; - default: - return; - } - })} -
-
- ); - } - - return {children}; -}); - -const BadgePill = ({ badge, parentProps, type }: any) => { - const styling: { css?: StylingCSS } = {}; - if (!parentProps.disableStyles) { - styling.css = [CSS.BadgePill({ ...parentProps, ...badge, type }), parentProps.style]; - } else if (parentProps.style) { - styling.css = [parentProps.style]; - } - - return ( -
- {badge.label} -
- ); -}; - -const BadgeImage = ({ badge, parentProps, type }: any) => { - const styling: { css?: StylingCSS } = {}; - if (!parentProps.disableStyles) { - styling.css = [CSS.BadgeImage({ ...parentProps, ...badge, type }), parentProps.style]; - } else if (parentProps.style) { - styling.css = [parentProps.style]; - } - return ( - {badge.label} - ); -}; - -export interface SelfServeBadgeProps extends ComponentProps { - result: Product; - controller?: AbstractController | SearchController; - - children?: ComponentChildren; - callout?: string; -} diff --git a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/index.ts b/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/index.ts deleted file mode 100644 index 1eebea509..000000000 --- a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SelfServeBadge'; diff --git a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/readme.md b/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/readme.md deleted file mode 100644 index c19bf3e99..000000000 --- a/packages/snap-preact-components/src/components/Atoms/SelfServeBadge/readme.md +++ /dev/null @@ -1,2 +0,0 @@ -# SelfServeBadge - diff --git a/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.stories.tsx b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.stories.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.stories.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.test.tsx b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.test.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.test.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.tsx b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.tsx new file mode 100644 index 000000000..bcb53e832 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/CalloutBadge.tsx @@ -0,0 +1,69 @@ +/** @jsx jsx */ +import { Fragment, h } from 'preact'; + +import { jsx, css } from '@emotion/react'; +import classnames from 'classnames'; +import { observer } from 'mobx-react-lite'; + +import { Theme, useTheme, CacheProvider } from '../../../providers'; +import { ComponentProps, StylingCSS, ComponentMap } from '../../../types'; +import { defaultBadgeComponentMap } from '../../../utilities'; + +import type { AutocompleteController, RecommendationController, SearchController } from '@searchspring/snap-controller'; +import type { Product } from '@searchspring/snap-store-mobx'; + +const CSS = { + CalloutBadge: ({}: CalloutBadgeProps) => + css({ + display: 'inline-block', + }), +}; + +export const CalloutBadge = observer((properties: CalloutBadgeProps): JSX.Element => { + const globalTheme: Theme = useTheme(); + + const props: CalloutBadgeProps = { + // default props + // global theme + ...globalTheme?.components?.calloutBadge, + // props + ...properties, + ...properties.theme?.components?.calloutBadge, + }; + const { result, name, controller, disableStyles, className, style } = props; + + const styling: { css?: StylingCSS } = {}; + + const badgeComponentMap = Object.assign(defaultBadgeComponentMap, props.componentMap || {}); + + if (!disableStyles) { + styling.css = [CSS.CalloutBadge(props), style]; + } else if (style) { + styling.css = [style]; + } + + const calloutBadge = controller.store.badges.getCalloutBadge(result, name); + + if (calloutBadge) { + const BadgeComponent = badgeComponentMap[calloutBadge.component]; + if (!BadgeComponent) { + controller?.log?.warn(`Badge component not found for ${calloutBadge.component}`); + return ; + } + return ( + +
+ +
+
+ ); + } + return ; +}); + +export interface CalloutBadgeProps extends ComponentProps { + result: Product; + controller: SearchController | AutocompleteController | RecommendationController; + name: string; + componentMap?: ComponentMap; +} diff --git a/packages/snap-preact-components/src/components/Molecules/CalloutBadge/index.ts b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/index.ts new file mode 100644 index 000000000..f2a6241d3 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/index.ts @@ -0,0 +1 @@ +export * from './CalloutBadge'; diff --git a/packages/snap-preact-components/src/components/Molecules/CalloutBadge/readme.md b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/readme.md new file mode 100644 index 000000000..3663814b6 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/CalloutBadge/readme.md @@ -0,0 +1,2 @@ +# CalloutBadge // TODO + diff --git a/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.stories.tsx b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.stories.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.stories.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.test.tsx b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.test.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.test.tsx @@ -0,0 +1 @@ +// TODO diff --git a/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.tsx b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.tsx new file mode 100644 index 000000000..f07566719 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/OverlayBadge.tsx @@ -0,0 +1,105 @@ +/** @jsx jsx */ +import { ComponentChildren, Fragment, h } from 'preact'; + +import { jsx, css } from '@emotion/react'; +import type { CSSObject } from '@emotion/serialize'; +import classnames from 'classnames'; +import { observer } from 'mobx-react-lite'; + +import { Theme, useTheme, CacheProvider } from '../../../providers'; +import { ComponentProps, StylingCSS, ComponentMap } from '../../../types'; +import { defaultBadgeComponentMap } from '../../../utilities'; + +import type { AutocompleteController, RecommendationController, SearchController } from '@searchspring/snap-controller'; +import type { Product, OverlayResultBadge } from '@searchspring/snap-store-mobx'; + +const CSS = { + OverlayBadge: ({}: OverlayBadgeProps) => { + return css({ + position: 'relative', + }); + }, + BadgePositioning: (props: OverlayResultBadge) => { + const { location, overlayLocation, overlayLocationOptions } = props; + let positioningStyles: CSSObject = {}; + + const index = overlayLocationOptions.findIndex((option: any) => option.name === location); + if (index === 0) { + // first option, assume top + positioningStyles = { top: 0 }; + } else if (index === overlayLocationOptions.length - 1) { + // last in list, assume bottom + positioningStyles = { bottom: 0 }; + } else if (index > 0 && index < overlayLocationOptions.length - 1) { + // middle options, calculate percentage based on position in list + const percentage = (index / (overlayLocationOptions.length - 1)) * 100; + positioningStyles = { top: `${percentage}%`, transform: `translateY(-${percentage}%)`, margin: 'auto' }; + } + + return css({ + [overlayLocation]: 0, // left or right + position: 'absolute', + maxWidth: '50%', + boxSizing: 'border-box', + ...positioningStyles, + }); + }, +}; + +export const OverlayBadge = observer((properties: OverlayBadgeProps): JSX.Element => { + const globalTheme: Theme = useTheme(); + + const props: OverlayBadgeProps = { + // default props + // global theme + ...globalTheme?.components?.overlayBadge, + // props + ...properties, + ...properties.theme?.components?.overlayBadge, + }; + const { result, children, controller, disableStyles, className, style } = props; + + const styling: { css?: StylingCSS } = {}; + + if (!children) { + controller?.log?.warn('OverlayBadge component must have children'); + return ; + } + + const badgeComponentMap = Object.assign(defaultBadgeComponentMap, props.componentMap || {}); + + if (!disableStyles) { + styling.css = [CSS.OverlayBadge(props), style]; + } else if (style) { + styling.css = [style]; + } + + const overlayBadges = controller.store.badges.getOverlayBadges(result); + + if (overlayBadges?.length) { + return ( + +
+ {children} + {overlayBadges.map((badge) => { + const BadgeComponent = badgeComponentMap[badge?.component]; + if (!BadgeComponent) { + controller?.log?.warn(`Badge component not found for ${badge?.component}`); + return; + } + return ; + })} +
+
+ ); + } + + return {children}; +}); + +export interface OverlayBadgeProps extends ComponentProps { + result: Product; + controller: SearchController | AutocompleteController | RecommendationController; + children: ComponentChildren; + componentMap?: ComponentMap; +} diff --git a/packages/snap-preact-components/src/components/Molecules/OverlayBadge/index.ts b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/index.ts new file mode 100644 index 000000000..efbc84f73 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/index.ts @@ -0,0 +1 @@ +export * from './OverlayBadge'; diff --git a/packages/snap-preact-components/src/components/Molecules/OverlayBadge/readme.md b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/readme.md new file mode 100644 index 000000000..8d3bb0960 --- /dev/null +++ b/packages/snap-preact-components/src/components/Molecules/OverlayBadge/readme.md @@ -0,0 +1,2 @@ +# OverlayBadge // TODO + diff --git a/packages/snap-preact-components/src/components/Molecules/Result/Result.tsx b/packages/snap-preact-components/src/components/Molecules/Result/Result.tsx index c7f8935de..64f125ffa 100644 --- a/packages/snap-preact-components/src/components/Molecules/Result/Result.tsx +++ b/packages/snap-preact-components/src/components/Molecules/Result/Result.tsx @@ -6,13 +6,13 @@ import { jsx, css } from '@emotion/react'; import classnames from 'classnames'; import { Image, ImageProps } from '../../Atoms/Image'; -import { BadgeProps } from '../../Atoms/Badge'; import { Price, PriceProps } from '../../Atoms/Price'; import { Theme, useTheme, CacheProvider } from '../../../providers'; import { defined, cloneWithProps } from '../../../utilities'; import { filters } from '@searchspring/snap-toolbox'; import { ComponentProps, LayoutType, Layout, StylingCSS } from '../../../types'; -import { SelfServeBadge } from '../../Atoms/SelfServeBadge'; +import { CalloutBadge, CalloutBadgeProps } from '../../Molecules/CalloutBadge'; +import { OverlayBadge, OverlayBadgeProps } from '../../Molecules/OverlayBadge'; import type { SearchController, AutocompleteController, RecommendationController } from '@searchspring/snap-controller'; import type { Product } from '@searchspring/snap-store-mobx'; @@ -104,12 +104,23 @@ export const Result = observer((properties: ResultProps): JSX.Element => { // component theme overrides theme: props.theme, }, - badge: { + calloutBadge: { // default props - className: 'ss__result__badge', - content: 'Sale', + className: 'ss__result__callout-badge', // global theme - ...globalTheme?.components?.badge, + ...globalTheme?.components?.calloutBadge, + // inherited props + ...defined({ + disableStyles, + }), + // component theme overrides + theme: props.theme, + }, + overlayBadge: { + // default props + className: 'ss__result__overlay-badge', + // global theme + ...globalTheme?.components?.overlayBadge, // inherited props ...defined({ disableStyles, @@ -134,7 +145,6 @@ export const Result = observer((properties: ResultProps): JSX.Element => { }, }; - // const onSale = Boolean(core?.msrp && core.price && core?.msrp * 1 > core?.price * 1); let displayName = core?.name; if (props.truncateTitle) { displayName = filters.truncate(core?.name || '', props.truncateTitle.limit, props.truncateTitle.append); @@ -147,41 +157,48 @@ export const Result = observer((properties: ResultProps): JSX.Element => { styling.css = [style]; } + const ImageWrapper = () => { + return ( + + ); + }; + return core ? (
{!hideBadge ? ( - - - + + + ) : ( - + )}
@@ -218,7 +234,8 @@ export const Result = observer((properties: ResultProps): JSX.Element => { }); interface ResultSubProps { - badge: BadgeProps; + calloutBadge: CalloutBadgeProps; + overlayBadge: OverlayBadgeProps; price: PriceProps; image: ImageProps; } diff --git a/packages/snap-preact-components/src/index.ts b/packages/snap-preact-components/src/index.ts index 5871db84e..604416d5a 100644 --- a/packages/snap-preact-components/src/index.ts +++ b/packages/snap-preact-components/src/index.ts @@ -10,12 +10,12 @@ export * from './components/Atoms/Loading'; export * from './components/Atoms/Merchandising'; export * from './components/Atoms/Overlay'; export * from './components/Atoms/Price'; -export * from './components/Atoms/SelfServeBadge'; export * from './components/Atoms/Skeleton'; export * from './components/Trackers/Recommendation/ResultTracker'; export * from './components/Trackers/Recommendation/ProfileTracker'; // MOLECULES +export * from './components/Molecules/CalloutBadge'; export * from './components/Molecules/Carousel'; export * from './components/Molecules/Checkbox'; export * from './components/Molecules/ErrorHandler'; @@ -24,6 +24,7 @@ export * from './components/Molecules/FacetHierarchyOptions'; export * from './components/Molecules/FacetListOptions'; export * from './components/Molecules/FacetPaletteOptions'; export * from './components/Molecules/Filter'; +export * from './components/Molecules/OverlayBadge'; export * from './components/Molecules/Pagination'; export * from './components/Molecules/Result'; export * from './components/Molecules/Select'; diff --git a/packages/snap-preact-components/src/types.ts b/packages/snap-preact-components/src/types.ts index 575099dea..052b52173 100644 --- a/packages/snap-preact-components/src/types.ts +++ b/packages/snap-preact-components/src/types.ts @@ -9,6 +9,10 @@ export interface ComponentProps extends RenderableProps { theme?: Theme; } +export type ComponentMap = { + [key: string]: (args?: any) => JSX.Element; +}; + export enum Layout { GRID = 'grid', LIST = 'list', diff --git a/packages/snap-preact-components/src/utilities/defaultBadgeComponentMap.ts b/packages/snap-preact-components/src/utilities/defaultBadgeComponentMap.ts new file mode 100644 index 000000000..4e8c7f924 --- /dev/null +++ b/packages/snap-preact-components/src/utilities/defaultBadgeComponentMap.ts @@ -0,0 +1,11 @@ +import { BadgePill } from '../components/Atoms/BadgePill'; +import { BadgeImage } from '../components/Atoms/BadgeImage'; +import { ComponentMap } from '../types'; + +export const defaultBadgeComponentMap: ComponentMap = { + BadgePill: BadgePill, // TODO: remove these + 'searchspring/pill': BadgePill, + + BadgeImage: BadgeImage, // TODO: remove these + 'searchspring/image': BadgeImage, +}; diff --git a/packages/snap-preact-components/src/utilities/index.ts b/packages/snap-preact-components/src/utilities/index.ts index 18d6190bd..14cea20f7 100644 --- a/packages/snap-preact-components/src/utilities/index.ts +++ b/packages/snap-preact-components/src/utilities/index.ts @@ -4,3 +4,4 @@ export * from './componentArgs'; export * from './sprintf'; export * from './LightenDarkenColor'; export * from './shiftColor'; +export * from './defaultBadgeComponentMap'; diff --git a/packages/snap-shared/src/MockData/MockData.ts b/packages/snap-shared/src/MockData/MockData.ts index 34668d750..8babba093 100644 --- a/packages/snap-shared/src/MockData/MockData.ts +++ b/packages/snap-shared/src/MockData/MockData.ts @@ -122,6 +122,7 @@ export class MockData { const profileFile = `${__dirname}/recommend/profile/${this.config.siteId}/${files?.profileFile || this.config.recommend?.profile}.json`; const resultsFile = `${__dirname}/recommend/results/${this.config.siteId}/${files?.resultsFile || this.config.recommend?.results}.json`; return { + meta: this.meta(), profile: getJSON(profileFile).profile, results: getJSON(resultsFile)[0].results, }; diff --git a/packages/snap-store-mobx/src/Autocomplete/AutocompleteStore.ts b/packages/snap-store-mobx/src/Autocomplete/AutocompleteStore.ts index 10ac30008..a91969ddc 100644 --- a/packages/snap-store-mobx/src/Autocomplete/AutocompleteStore.ts +++ b/packages/snap-store-mobx/src/Autocomplete/AutocompleteStore.ts @@ -9,6 +9,7 @@ import { SearchPaginationStore, SearchSortingStore, SearchHistoryStore, + SearchBadgeStore, } from '../Search/Stores'; import { StorageStore } from '../Storage/StorageStore'; import { @@ -22,7 +23,7 @@ import { import type { AutocompleteResponseModel, MetaResponseModel } from '@searchspring/snapi-types'; import type { TrendingResponseModel } from '@searchspring/snap-client'; -import type { AutocompleteStoreConfig, StoreServices } from '../types'; +import type { AutocompleteStoreConfig, MetaBadges, StoreServices } from '../types'; export class AutocompleteStore extends AbstractStore { public services: StoreServices; @@ -39,6 +40,7 @@ export class AutocompleteStore extends AbstractStore { public storage: StorageStore; public trending: AutocompleteTrendingStore; public history: AutocompleteHistoryStore; + public badges!: SearchBadgeStore; constructor(config: AutocompleteStoreConfig, services: StoreServices) { super(config); @@ -192,5 +194,7 @@ export class AutocompleteStore extends AbstractStore { this.pagination = new SearchPaginationStore(this.config, this.services, data.pagination, this.meta); this.sorting = new SearchSortingStore(this.services, data.sorting || [], data.search || {}, this.meta); + // TODO: remove MetaBadges typing once MetaResponseModel is updated + this.badges = new SearchBadgeStore(this.meta as MetaResponseModel & { badges: MetaBadges }); } } diff --git a/packages/snap-store-mobx/src/Finder/FinderStore.ts b/packages/snap-store-mobx/src/Finder/FinderStore.ts index 2dd749693..a71f8be84 100644 --- a/packages/snap-store-mobx/src/Finder/FinderStore.ts +++ b/packages/snap-store-mobx/src/Finder/FinderStore.ts @@ -11,7 +11,7 @@ import { UrlManager } from '@searchspring/snap-url-manager'; export class FinderStore extends AbstractStore { public services: StoreServices; public config!: FinderStoreConfig; - public meta: MetaResponseModel = {}; + public meta!: MetaResponseModel; public storage: StorageStore; public persistedStorage!: StorageStore; public pagination!: SearchPaginationStore; @@ -94,10 +94,10 @@ export class FinderStore extends AbstractStore { } } - public update(data: SearchResponseModel & { meta: MetaResponseModel }, selectedSelections?: SelectedSelection[]): void { + public update(data: SearchResponseModel & { meta?: MetaResponseModel }, selectedSelections?: SelectedSelection[]): void { this.error = undefined; this.loaded = !!data.pagination; - this.meta = data.meta; + this.meta = data.meta || {}; this.pagination = new SearchPaginationStore(this.config, this.services, data.pagination, this.meta); this.selections = new FinderSelectionStore(this.config, this.services, { state: this.state, diff --git a/packages/snap-store-mobx/src/Recommendation/RecommendationStore.ts b/packages/snap-store-mobx/src/Recommendation/RecommendationStore.ts index db2445098..5fff9b6d4 100644 --- a/packages/snap-store-mobx/src/Recommendation/RecommendationStore.ts +++ b/packages/snap-store-mobx/src/Recommendation/RecommendationStore.ts @@ -1,15 +1,18 @@ import { makeObservable, observable } from 'mobx'; import { AbstractStore } from '../Abstract/AbstractStore'; -import { SearchResultStore } from '../Search/Stores'; +import { SearchBadgeStore, SearchResultStore } from '../Search/Stores'; import { RecommendationProfileStore } from './Stores'; -import type { RecommendationStoreConfig, StoreServices } from '../types'; +import type { MetaBadges, RecommendationStoreConfig, StoreServices } from '../types'; import type { RecommendCombinedResponseModel } from '@searchspring/snap-client'; +import { MetaResponseModel } from '@searchspring/snapi-types'; export class RecommendationStore extends AbstractStore { public services: StoreServices; + public meta!: MetaResponseModel; public loaded = false; public profile!: RecommendationProfileStore; public results!: SearchResultStore; + public badges!: SearchBadgeStore; constructor(config: RecommendationStoreConfig, services: StoreServices) { super(config); @@ -32,10 +35,13 @@ export class RecommendationStore extends AbstractStore { this.update(); } - public update(data?: RecommendCombinedResponseModel): void { + public update(data?: RecommendCombinedResponseModel & { meta?: MetaResponseModel }): void { this.error = undefined; this.loaded = !!data?.profile; + this.meta = data?.meta || {}; this.profile = new RecommendationProfileStore(this.services, data); this.results = new SearchResultStore(this.config, this.services, {}, data?.results); + // TODO: remove MetaBadges typing once MetaResponseModel is updated + this.badges = new SearchBadgeStore(this.meta as MetaResponseModel & { badges: MetaBadges }); } } diff --git a/packages/snap-store-mobx/src/Search/SearchStore.ts b/packages/snap-store-mobx/src/Search/SearchStore.ts index a71b51021..a34eca869 100644 --- a/packages/snap-store-mobx/src/Search/SearchStore.ts +++ b/packages/snap-store-mobx/src/Search/SearchStore.ts @@ -1,7 +1,7 @@ import { makeObservable, observable } from 'mobx'; import type { SearchResponseModel, MetaResponseModel } from '@searchspring/snapi-types'; -import type { SearchStoreConfig, StoreServices } from '../types'; +import type { MetaBadges, SearchStoreConfig, StoreServices } from '../types'; import { SearchMerchandisingStore, SearchFacetStore, @@ -11,6 +11,7 @@ import { SearchSortingStore, SearchQueryStore, SearchHistoryStore, + SearchBadgeStore, } from './Stores'; import type { HistoryStoreConfig } from './Stores/SearchHistoryStore'; import { AbstractStore } from '../Abstract/AbstractStore'; @@ -28,6 +29,7 @@ export class SearchStore extends AbstractStore { public sorting!: SearchSortingStore; public storage: StorageStore; public history: SearchHistoryStore; + public badges!: SearchBadgeStore; constructor(config: SearchStoreConfig, services: StoreServices) { super(config); @@ -101,5 +103,7 @@ export class SearchStore extends AbstractStore { this.results = new SearchResultStore(this.config, this.services, this.meta, data?.results || [], data.pagination, data.merchandising); this.pagination = new SearchPaginationStore(this.config, this.services, data.pagination, this.meta); this.sorting = new SearchSortingStore(this.services, data?.sorting || [], data?.search || {}, this.meta); + // TODO: remove MetaBadges typing once MetaResponseModel is updated + this.badges = new SearchBadgeStore(this.meta as MetaResponseModel & { badges: MetaBadges }); } } diff --git a/packages/snap-store-mobx/src/Search/Stores/SearchBadgeStore.ts b/packages/snap-store-mobx/src/Search/Stores/SearchBadgeStore.ts new file mode 100644 index 000000000..6ecdca6c6 --- /dev/null +++ b/packages/snap-store-mobx/src/Search/Stores/SearchBadgeStore.ts @@ -0,0 +1,51 @@ +import { MetaResponseModel } from '@searchspring/snapi-types'; +import { Product } from './SearchResultStore'; +import type { MetaBadges, ResultBadge, OverlayResultBadge } from '../../types'; + +export class SearchBadgeStore { + private meta: MetaResponseModel & { badges: MetaBadges }; + + constructor(meta: MetaResponseModel & { badges: MetaBadges }) { + this.meta = meta; + } + + getCalloutBadge(result: Product, name: string): ResultBadge | undefined { + const resultBadges = result.badges; + const callouts = this.meta?.badges?.locations?.callouts; + + if (!resultBadges?.length || !callouts) { + return; + } + + return resultBadges.find((badge) => { + return callouts?.some((callout) => callout.name === badge.location) && badge.location === name; + }); + } + + getOverlayBadges(result: Product): OverlayResultBadge[] | undefined { + const resultBadges = result.badges; + const overlay = this.meta.badges?.locations?.overlay; + + if (!resultBadges?.length || !overlay) { + return; + } + + return resultBadges + ?.map((badge) => { + // add overlayLocation and overlayLocationOptions properties to be used in styling + const isRightOverlay = overlay.right.some((rightOverlays) => rightOverlays.name === badge.location); + const isLeftOverlay = overlay.left.some((leftOverlays) => leftOverlays.name === badge.location); + const overlayLocation = isRightOverlay ? 'right' : isLeftOverlay ? 'left' : ''; + + return { + ...badge, + overlayLocation, + overlayLocationOptions: overlay[overlayLocation as keyof typeof overlay], + }; + }) + .filter((badge) => { + // filter out badges that are not overlay badges + return badge.overlayLocation; + }); + } +} diff --git a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts index dd3874446..295e009eb 100644 --- a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts +++ b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts @@ -1,6 +1,6 @@ import { makeObservable, observable } from 'mobx'; -import type { SearchStoreConfig, StoreServices, StoreConfigs } from '../../types'; +import type { SearchStoreConfig, StoreServices, StoreConfigs, ResultBadge, MetaBadges } from '../../types'; import type { SearchResponseModelResult, SearchResponseModelPagination, @@ -55,7 +55,7 @@ export class Banner { public custom = {}; public config: SearchResponseModelMerchandisingContentConfig; public value: string; - public badges: any; + public badges: ResultBadge[] = []; constructor(services: StoreServices, banner: SearchResponseModelMerchandisingContentInline) { this.id = 'ss-ib-' + banner.config!.position!.index; @@ -70,39 +70,6 @@ export class Banner { } } -export type ResultBadge = { - tag: string; - label: string; -}; - -export type MetaBadges = { - locations: { - overlay: { - [position: string]: { - name: string; - label: string; - description: string; - }[]; - }; - callouts: { - name: string; - label: string; - description: string; - }[]; - }; - tags: { - [tag: string]: { - component: string; - location: string; - parameters: { - color: string; - colorText: string; - url: string; - }; - }; - }; -}; - export class Product { public type = 'product'; public id: string; @@ -112,7 +79,7 @@ export class Product { }; public custom = {}; public children?: Array = []; - public badges: any; + public badges: ResultBadge[] = []; constructor(services: StoreServices, result: SearchResponseModelResult, metaData: MetaResponseModel) { this.id = result.id!; diff --git a/packages/snap-store-mobx/src/Search/Stores/index.ts b/packages/snap-store-mobx/src/Search/Stores/index.ts index a3939aab1..bb7552ce2 100644 --- a/packages/snap-store-mobx/src/Search/Stores/index.ts +++ b/packages/snap-store-mobx/src/Search/Stores/index.ts @@ -6,3 +6,4 @@ export { SearchResultStore, Product, Banner } from './SearchResultStore'; export { SearchSortingStore } from './SearchSortingStore'; export { SearchQueryStore } from './SearchQueryStore'; export { SearchHistoryStore } from './SearchHistoryStore'; +export { SearchBadgeStore } from './SearchBadgeStore'; diff --git a/packages/snap-store-mobx/src/types.ts b/packages/snap-store-mobx/src/types.ts index 4ed54f770..67d121ca4 100644 --- a/packages/snap-store-mobx/src/types.ts +++ b/packages/snap-store-mobx/src/types.ts @@ -127,3 +127,39 @@ export type SelectedSelection = { export type FinderStoreState = { persisted: boolean; }; + +export type BadgeTag = { + location: string; + component: string; + parameters: { + [key: string]: string; + }; +}; +export type BadgeLocation = { + name: string; + label: string; + description: string; +}; + +export type ResultBadge = BadgeTag & { + tag: string; + label: string; +}; + +export type OverlayResultBadge = ResultBadge & { + overlayLocation: string; + overlayLocationOptions: BadgeLocation[]; +}; + +export type MetaBadges = { + locations: { + overlay: { + left: BadgeLocation[]; + right: BadgeLocation[]; + }; + callouts: BadgeLocation[]; + }; + tags: { + [key: string]: BadgeTag; + }; +};