Skip to content

Commit

Permalink
feat(preact-components-bundlerecommendations): new component for upco…
Browse files Browse the repository at this point in the history
…ming bundlerecs feature
  • Loading branch information
chrisFrazier77 committed Dec 19, 2023
1 parent e97cb34 commit 5547094
Show file tree
Hide file tree
Showing 18 changed files with 74,227 additions and 72,437 deletions.
144,793 changes: 72,387 additions & 72,406 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ import type { RecommendationStore } from '@searchspring/snap-store-mobx';
import type { Next } from '@searchspring/snap-event-manager';
import type { RecommendationControllerConfig, BeforeSearchObj, AfterStoreObj, ControllerServices, ContextVariables } from '../types';

export interface selectedItem {
id: string;
quantity: number;
}

type RecommendationTrackMethods = {
product: {
click: (e: MouseEvent, result: any) => BeaconEvent | undefined;
render: (result: any) => BeaconEvent | undefined;
impression: (result: any) => BeaconEvent | undefined;
};
click: (e: MouseEvent) => BeaconEvent | undefined;
addBundleToCart: (e: MouseEvent, results: selectedItem[], price: number) => BeaconEvent | undefined;
impression: () => BeaconEvent | undefined;
render: (results?: Product[]) => BeaconEvent | undefined;
};
Expand Down Expand Up @@ -206,6 +212,34 @@ export class RecommendationController extends AbstractController {
return event;
},
},
addBundleToCart: (e: MouseEvent, results: selectedItem[], price: number): BeaconEvent | undefined => {
if (!this.store.profile.tag) return;
const event: BeaconEvent = this.tracker.track.event({
type: BeaconType.PROFILE_CLICK,
category: BeaconCategory.RECOMMENDATIONS,
context: this.config.globals.siteId ? { website: { trackingCode: this.config.globals.siteId } } : undefined,
event: {
context: {
action: 'navigate',
placement: this.store.profile.placement,
tag: this.store.profile.tag,
type: 'product-recommendation',
results: results,
price: price,
},
profile: {
tag: this.store.profile.tag,
placement: this.store.profile.placement,
threshold: this.store.profile.display.threshold,
templateId: this.store.profile.display.template.uuid,
seed: getSeed(),
},
},
});
this.events.click = event;
this.eventManager.fire('track.click', { controller: this, event: e, trackEvent: event });
return event;
},
click: (e: MouseEvent): BeaconEvent | undefined => {
if (!this.store.profile.tag) return;
const event: BeaconEvent = this.tracker.track.event({
Expand Down Expand Up @@ -284,7 +318,7 @@ export class RecommendationController extends AbstractController {
this.eventManager.fire('track.render', { controller: this, trackEvent: event });
return event;
},
};
} as RecommendationTrackMethods;
})();

get params(): RecommendCombinedRequestModel {
Expand Down
2 changes: 1 addition & 1 deletion packages/snap-preact-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"dequal": "2.0.3",
"mobx-react-lite": "3.4.0",
"react-ranger": "2.1.0",
"swiper": "6.8.4"
"swiper": "9.0.5"
},
"peerDependencies": {
"preact": "10.9.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/** @jsx jsx */
import { h, Fragment } from 'preact';
import { useRef } from 'preact/hooks';
import { useRef, useEffect } from 'preact/hooks';

import { jsx, css } from '@emotion/react';
import classnames from 'classnames';
import { observer } from 'mobx-react-lite';
import deepmerge from 'deepmerge';
import SwiperCore, { Pagination, Navigation, A11y } from 'swiper/core';
import { SwiperOptions } from 'swiper';
import { Icon, IconProps } from '../../Atoms/Icon/Icon';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, A11y } from 'swiper';
import { Icon, IconProps } from '../../Atoms/Icon/Icon';
import type { Swiper as SwiperTypes, SwiperOptions } from 'swiper';
import { defined } from '../../../utilities';
import { Theme, useTheme, CacheProvider } from '../../../providers';
import { ComponentProps, BreakpointsProps, StylingCSS } from '../../../types';
Expand Down Expand Up @@ -200,12 +200,11 @@ export const Carousel = observer((properties: CarouselProps): JSX.Element => {

const displaySettings = useDisplaySettings(props.breakpoints!);
if (displaySettings && Object.keys(displaySettings).length) {
const theme = deepmerge(props?.theme || {}, displaySettings?.theme || {}, { arrayMerge: (destinationArray, sourceArray) => sourceArray });
const theme = deepmerge(props?.theme || {}, displaySettings?.theme || {});

if (props.autoAdjustSlides && props.children.length < displaySettings.slidesPerView) {
if (props.autoAdjustSlides && props.children.length < displaySettings.slidesPerView!) {
displaySettings.slidesPerView = props.children.length;
displaySettings.slidesPerGroup = props.children.length;
displaySettings.loop = false;
}
props = {
...props,
Expand Down Expand Up @@ -253,8 +252,6 @@ export const Carousel = observer((properties: CarouselProps): JSX.Element => {
//remove any duplicates, in case user passes in Navigation or Pagination
const swiperModules = swiperModulesUnfiltered.filter((module, pos) => swiperModulesUnfiltered.indexOf(module) === pos);

SwiperCore.use(swiperModules);

const navigationPrevRef = useRef(null);
const navigationNextRef = useRef(null);
const rootComponentRef = useRef(null);
Expand All @@ -266,6 +263,17 @@ export const Carousel = observer((properties: CarouselProps): JSX.Element => {
styling.css = [style];
}

useEffect(() => {
//backwards compatability for legacy styles
const swipers = document.querySelectorAll('.swiper');
swipers.forEach((elem: any) => {
elem.classList.add('swiper-container');
});

//add usable class to last visible slide.
attachClasstoLastVisibleSlide();
}, []);

if (pagination) {
if (typeof pagination == 'object') {
pagination = {
Expand All @@ -279,6 +287,18 @@ export const Carousel = observer((properties: CarouselProps): JSX.Element => {
}
}

const attachClasstoLastVisibleSlide = () => {
const swiperElem = rootComponentRef.current as unknown as HTMLElement;
const slides_visible = swiperElem?.querySelectorAll('.swiper-slide-visible');

slides_visible.forEach((element, idx) => {
element.classList.remove('last-visible-slide');
if (idx == slides_visible.length - 1) {
element.classList.add('last-visible-slide');
}
});
};

return children?.length ? (
<CacheProvider>
<div
Expand All @@ -298,7 +318,7 @@ export const Carousel = observer((properties: CarouselProps): JSX.Element => {

<Swiper
centerInsufficientSlides={true}
onInit={(swiper) => {
onBeforeInit={(swiper) => {
//@ts-ignore : someone should refactor this
swiper.params.navigation.prevEl = navigationPrevRef.current ? navigationPrevRef.current : undefined;
//@ts-ignore : someone should refactor this
Expand All @@ -307,12 +327,20 @@ export const Carousel = observer((properties: CarouselProps): JSX.Element => {
onInit(swiper);
}
}}
onResize={() => {
attachClasstoLastVisibleSlide();
}}
onTransitionEnd={() => {
attachClasstoLastVisibleSlide();
}}
onClick={(swiper, e) => {
onClick && onClick(swiper, e);
}}
direction={vertical ? 'vertical' : 'horizontal'}
loop={loop}
threshold={7}
modules={swiperModules}
navigation
{...additionalProps}
{...displaySettings}
pagination={pagination}
Expand Down Expand Up @@ -347,10 +375,10 @@ export interface CarouselProps extends ComponentProps {
vertical?: boolean;
pagination?: boolean | SwiperOptions['pagination'];
autoAdjustSlides?: boolean;
onClick?: (swiper: SwiperCore, e: MouseEvent | TouchEvent | PointerEvent) => void;
onClick?: (swiper: SwiperTypes, e: MouseEvent | TouchEvent | PointerEvent) => void;
onNextButtonClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onPrevButtonClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onInit?: (swiper: SwiperCore) => void;
onInit?: (swiper: SwiperTypes) => void;
modules?: any[];
children: JSX.Element[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Snapify } from '../../../utilities/snapify';
import Readme from '../Result/readme.md';
import { Layout } from '../../../types';
import type { SearchController } from '@searchspring/snap-controller';
import { Product } from '@searchspring/snap-store-mobx';

export default {
title: `Molecules/Result`,
Expand Down Expand Up @@ -151,7 +152,7 @@ export default {
const snapInstance = Snapify.search({ id: 'Result', globals: { siteId: '8uyt2m' } });

export const Default = (args: ResultProps, { loaded: { controller } }: { loaded: { controller: SearchController } }) => (
<Result {...args} result={controller?.store?.results[0]} />
<Result {...args} result={controller?.store?.results[0] as Product} />
);

Default.loaders = [
Expand All @@ -164,7 +165,7 @@ Default.loaders = [
];

export const hideSections = (args: ResultProps, { loaded: { controller } }: { loaded: { controller: SearchController } }) => (
<Result {...args} result={controller?.store?.results[0]} />
<Result {...args} result={controller?.store?.results[0] as Product} />
);

hideSections.loaders = [
Expand All @@ -182,7 +183,7 @@ hideSections.args = {
};

export const truncateTitle = (args: ResultProps, { loaded: { controller } }: { loaded: { controller: SearchController } }) => (
<Result {...args} result={controller?.store?.results[0]} />
<Result {...args} result={controller?.store?.results[0] as Product} />
);

truncateTitle.loaders = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/** @jsx jsx */
import { h, Fragment } from 'preact';
import { jsx } from '@emotion/react';
import { observer } from 'mobx-react-lite';
import { cloneWithProps } from '../../../utilities';
import { Button } from '../../Atoms/Button';
import { Price } from '../../Atoms/Price';
import type { selectedItem } from './BundledRecommendations';

export const BundledCTA = observer((properties: BundledCTAProps): JSX.Element => {
const { ctaSlot, selectedItems, bundlePrice, bundleStrikePrice, addToCartFunc, addToCartText } = properties;

let totalNumProdsInBundle = 0;

selectedItems.forEach((item) => {
totalNumProdsInBundle += item.quantity;
});

return (
<div className={`ss__bundled-recommendations__product-wrapper__cta`}>
{ctaSlot ? (
cloneWithProps(ctaSlot, {
selectedItems: selectedItems,
bundlePrice: bundlePrice,
strikePrice: bundleStrikePrice,
onclick: (e: any) => addToCartFunc(e),
})
) : (
<Fragment>
<p className="ss__bundled-recommendations__product-wrapper__cta__subtotal">
{`Subtotal for ${totalNumProdsInBundle} items `}
<div className="ss__bundled-recommendations__product-wrapper__cta__subtotal__prices">
{bundleStrikePrice && bundleStrikePrice !== bundlePrice && (
<label className="ss__bundled-recommendations__product-wrapper__cta__subtotal__strike">
Was <Price lineThrough={true} value={bundleStrikePrice} />
</label>
)}
<label className="ss__bundled-recommendations__product-wrapper__cta__subtotal__price">
<Price value={bundlePrice} />
</label>
</div>
</p>

<Button onClick={(e) => addToCartFunc(e)}>{addToCartText}</Button>
</Fragment>
)}
</div>
);
});

interface BundledCTAProps {
isMobile: boolean;
ctaSlot?: JSX.Element;
selectedItems: selectedItem[];
bundlePrice: number;
bundleStrikePrice?: number;
addToCartFunc: (e: any) => void;
addToCartText?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/** @jsx jsx */
import { h, ComponentChildren } from 'preact';
import { jsx } from '@emotion/react';
import classnames from 'classnames';
import { observer } from 'mobx-react-lite';
import { Theme, useTheme } from '../../../providers';
import { Checkbox, CheckboxProps } from '../../Molecules/Checkbox';
import { Icon, IconProps } from '../../Atoms/Icon';

export const BundleSelector = observer((properties: BundleSelectorProps): JSX.Element => {
const globalTheme: Theme = useTheme();

const props: BundleSelectorProps = {
// default props
showCheckboxes: true,
qtyText: 'Qty:',
// global theme
...properties,
};

const { children, checked, quantity, icon, seedText, qtyText, showCheckboxes, onCheck, onInputChange } = props;

const subProps: BundleSelectorSubProps = {
icon: {
// default props
className: 'ss__bundled-recommendations__product-wrapper__selector__icon',
color: 'black',
size: 15,
// global theme
...globalTheme?.components?.icon,
},
checkbox: {
className: 'ss__bundled-recommendations__product-wrapper__selector__result-wrapper__checkbox',
checked: checked,
onClick: onCheck,
...globalTheme?.components?.checkbox,
},
};

return (
<div
className={classnames(
'ss__bundled-recommendations__product-wrapper__selector',
seedText ? 'ss__bundled-recommendations__product-wrapper__selector--seed' : ''
)}
>
<div className="ss__bundled-recommendations__product-wrapper__selector__result-wrapper">
{showCheckboxes && <Checkbox {...subProps.checkbox} />}
{seedText && <div className={'ss__bundled-recommendations__product-wrapper__selector__result-wrapper__seed-badge'}>{seedText}</div>}
{children}
{typeof quantity == 'number' && (
<div className="ss__bundled-recommendations__product-wrapper__selector__qty">
{qtyText}
<input
className="ss__bundled-recommendations__product-wrapper__selector__qty__input"
onChange={onInputChange}
aria-label="Product Quantity"
type="number"
min="0"
placeholder="QTY #"
value={quantity}
/>
</div>
)}
</div>
<Icon {...subProps.icon} {...(typeof icon == 'string' ? { icon: icon as string } : (icon as Partial<IconProps>))} />
</div>
);
});

export interface BundleSelectorSubProps {
icon: Partial<IconProps>;
checkbox: Partial<CheckboxProps>;
}

export interface BundleSelectorProps {
children?: ComponentChildren;
checked?: boolean;
quantity?: number;
seedText?: string;
showCheckboxes?: boolean;
qtyText?: string;
onCheck?: () => void;
onInputChange?: (e: any) => void;
icon?: string | Partial<IconProps> | boolean;
}
Loading

0 comments on commit 5547094

Please sign in to comment.