Skip to content

PWA-3318::Prex Compatility #4458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { string, shape, array } from 'prop-types';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
import GalleryItem from '@magento/venia-ui/lib/components/Gallery/item';
// inline loading of the css is janky, but the webpack loader gets blown out in local environment.
import defaultGalleryClasses from '!!style-loader!css-loader?modules!./gallery.css';
import defaultItemClasses from '!!style-loader!css-loader?modules!./item.css';

/**
* Renders a Gallery of items. If items is an array of nulls Gallery will render
* a placeholder item for each.
*
* @params {Array} props.items an array of items to render
*/
export const Gallery = props => {
const galleryClasses = mergeClasses(
defaultGalleryClasses,
props.galleryClasses
);
const itemClasses = mergeClasses(defaultItemClasses, props.itemClasses);

const { items } = props;

const galleryItems = items.map((item, index) => {
if (item === null) {
return <GalleryItem key={index} />;
}
return <GalleryItem key={item.id} item={item} classes={itemClasses} />;
});

return (
<div className={galleryClasses.root}>
<div className={galleryClasses.items}>{galleryItems}</div>
</div>
);
};

Gallery.propTypes = {
galleryClasses: shape({
filters: string,
items: string,
root: string
}),
itemClasses: shape({
image: string,
imageContainer: string,
imagePlaceholder: string,
image_pending: string,
images: string,
images_pending: string,
name: string,
name_pending: string,
price: string,
price_pending: string,
root: string,
root_pending: string
}),
items: array.isRequired
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.root {
display: grid;
grid-template-areas:
'actions'
'items';
grid-template-columns: 1fr;
line-height: 1;
}

.items {
grid-template-columns: repeat(5, 1fr);
margin-left: 2em;
margin-right: 2em;
margin-bottom: 60px;
display: grid;
grid-area: items;
grid-gap: 1rem;
}

@media (max-width: 640px) {
.items {
grid-template-columns: repeat(2, 1fr);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Gallery';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.name {
font-weight: bold;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.unitTitle {
text-align: center;
margin: 1.5em;
font-weight: 700;
font-size: 2.25rem;
font-family: 'Source Serif Pro';
}

.root a {
text-decoration: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React, { useRef } from 'react';
import { string, shape } from 'prop-types';

import useRecsTrackingProps from '../../hooks/useRecsTrackingProps';
import { Gallery } from '../Gallery/Gallery';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
// inline loading of the css is janky, but the webpack loader gets blown out in local environment.
import defaultClasses from '!!style-loader!css-loader?modules!./ProductRecommendations.css';
import useObserver from '../../hooks/useObserver';
import { mse } from '@magento/venia-data-collector';

export const VeniaProductRecommendations = props => {
const rendered = useRef([]);
const { units } = useRecsTrackingProps(props);
const { observeUnit } = useObserver();

const classes = mergeClasses(defaultClasses, props.classes);
const galleryClasses = mergeClasses(defaultClasses, props.galleryClasses);
const itemClasses = mergeClasses(defaultClasses, props.itemClasses);

const galleryUnits = units.map(recommendationUnit => {
if (recommendationUnit.totalProducts < 1) {
return null;
}

const items = recommendationUnit.products.map(shapeItem);
return (
<div
key={recommendationUnit.unitId}
data-unit-id={recommendationUnit.unitId}
className={classes.root}
ref={element => observeUnit(recommendationUnit, element)}
>
<div className={classes.unitTitle}>
{recommendationUnit.storefrontLabel}
</div>
<Gallery
galleryClasses={galleryClasses}
itemClasses={itemClasses}
items={items}
/>
</div>
);
});

if (units && units.length > 0) {
units.forEach(recUnit => {
if (
recUnit.totalProducts > 0 &&
!rendered.current.includes(recUnit.unitId)
) {
mse.publish.recsUnitRender(recUnit.unitId);
rendered.current.push(recUnit.unitId);
}
});

return <div>{galleryUnits}</div>;
} else {
return null;
}
};

VeniaProductRecommendations.propTypes = {
galleryClasses: shape({
filters: string,
items: string,
root: string
}),
itemClasses: shape({
image: string,
imageContainer: string,
imagePlaceholder: string,
image_pending: string,
images: string,
images_pending: string,
name: string,
name_pending: string,
price: string,
price_pending: string,
root: string,
root_pending: string
}),
classes: shape({
unitTitle: string,
root: string
}),
pageType: string.isRequired
};

// format data for GalleryItem, exported for testing
export const shapeItem = item => {
if (item) {
const { url, image, prices, productId, currency, type } = item;

// derive the url_key and url_suffix from the url
// example url --> https://magento.com/blah/blah/url_key.url_suffix
const urlArray = String(url)
.split('/')
.splice(-1)[0]
.split('.');
const url_key = urlArray[0] + `.${urlArray[1]}`;
const url_suffix = `.${urlArray[1]}`;

const price = {
regularPrice: {
amount: {
value: prices.minimum.regular,
currency
}
}
};

return {
...item,
id: productId,
small_image: image,
url_key,
url_suffix,
price,
// use inStock when the recs service provides it, use the commented out line below:
// stock_status: inStock ? "IN_STOCK" : "OUT_OF_STOCK";
stock_status: 'IN_STOCK',
type_id: type
};
} else return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ProductRecommendations';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './localStorageConstants';
export * from './pageTypes';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const USER_VIEW_HISTORY_TIME_DECAY_KEY = 'ds-view-history-time-decay';
export const USER_VIEW_HISTORY_KEY = 'ds-view-history';
export const PURCHASE_HISTORY_KEY = 'ds-purchase-history';
export const CART_CONTENTS_KEY = 'ds-cart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const CMS = 'CMS';
export const CART = 'Cart';
export const CATEGORY = 'Category';
export const PRODUCT = 'Product';
export const CHECKOUT = 'Checkout';
export const PAGEBUILDER = 'PageBuilder';

export const PageTypes = [CMS, CART, CATEGORY, CHECKOUT, PRODUCT, PAGEBUILDER];
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { mse } from '@magento/venia-data-collector';

const cleared = {};

// oddly, these functions error when not wrapped in a hook. 🤷
const useObserver = () => {
const meetThreshold = (entries, unit) => {
entries.forEach(entry => {
const { isIntersecting, intersectionRatio } = entry;
const { unitId } = unit;

if (!isIntersecting) {
cleared[unitId] = true;
}
if (cleared[unitId] !== false && intersectionRatio >= 0.5) {
cleared[unitId] = false;
mse.publish.recsUnitView(unit.unitId);
}
});
};

const observeUnit = (unit, element) => {
if (element) {
const options = {
threshold: [0.0, 0.5]
};
const observer = new IntersectionObserver(
entries => meetThreshold(entries, unit),
options
);
observer.observe(element);
} else {
console.warn(
'VeniaProductRecommendations IntersectionObserver: Element is either null or undefined.'
);
}
};
return { observeUnit };
};

export default useObserver;
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import RecommendationsClient from '@magento/recommendations-js-sdk';
import { useEffect, useState, useRef } from 'react';
import { PageTypes, PRODUCT } from '../constants';
import { mse } from '@magento/venia-data-collector';

const useRecsData = props => {
const [recs, setRecs] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fired = useRef(false);
const stale = useRef(false);
const [currentProduct, setCurrentProduct] = useState(null);

if (
(process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test') &&
(!props || !props.pageType)
) {
throw new Error(
'Headless Recommendations: PageType is required to fetch recommendations.'
);
} else if (props.pageType && !PageTypes.includes(props.pageType)) {
throw new Error(
`Headless Recommendations: ${
props.pageType
} is not a valid pagetype. Valid types include ${JSON.stringify(
PageTypes
)}`
);
}
const { pageType } = props;
const storefrontContext = mse.context.getStorefrontInstance();
const product = mse.context.getProduct();

useEffect(() => {
const fetchRecs = async () => {
const storefront = { ...storefrontContext, pageType };

const client = new RecommendationsClient(storefront);

let currentSku;
if (pageType === PRODUCT) {
currentSku = product.sku;
}

const fetchProps = {
...props,
currentSku
};
let res;

try {
setIsLoading(true);
fired.current = true;
stale.current = false;
mse.publish.recsRequestSent({ pageContext: { pageType } });

res = await client.fetchPreconfigured(fetchProps);
} catch (e) {
console.error(e);
setIsLoading(false);
setError(e);
}
if (res) {
const { data } = res;
mse.context.setRecommendations({ units: data.results });
mse.publish.recsResponseReceived();
setIsLoading(false);
setRecs(data);
}
};
if (
((!fired.current && !recs) || stale.current) &&
PageTypes.includes(pageType) &&
storefrontContext !== undefined &&
storefrontContext.environmentId &&
((pageType === PRODUCT &&
product !== undefined &&
product.sku !== undefined) ||
pageType !== PRODUCT)
) {
fetchRecs();
}
}, [pageType, props, recs, storefrontContext, product]);

useEffect(() => {
if (
product &&
product.sku &&
(!currentProduct || product.sku !== currentProduct.sku)
) {
setCurrentProduct(product);
}
}, [product, currentProduct]);

useEffect(() => {
if (currentProduct && recs && fired.current === true) {
stale.current = true;
}
}, [currentProduct, recs]);

return { data: recs, isLoading, error };
};

export default useRecsData;
Loading
Oops, something went wrong.