import React, { Component, ComponentClass } from 'react'; import { compose } from 'redux'; import { cloneDeep, isEqual, set } from 'lodash-es'; import { CommerceTypes, FetchDataFunction, ReviewDataSource, ReviewTypes, withCommerceData, WithCommerceProps, WithCommerceProviderProps } from '@brandingbrand/fscommerce'; // TODO: This should move into fscommerce export type CommerceToReviewMapFunction< T extends CommerceTypes.Product = CommerceTypes.Product > = (product: T) => string; /** * Additional props that are consumed by the high order component. * * @template T The type of product data that will be provided. Defaults to `Product` */ export interface WithProductDetailProviderProps< T extends CommerceTypes.Product = CommerceTypes.Product > extends WithCommerceProviderProps { commerceToReviewMap: keyof T | CommerceToReviewMapFunction; reviewDataSource?: ReviewDataSource; } /** * Additional props that will be provided to the wrapped component. * * @template T The type of product data that will be provided. Defaults to `Product` */ export type WithProductDetailProps< T extends CommerceTypes.Product = CommerceTypes.Product > = WithCommerceProps & { reviewsData?: ReviewTypes.ReviewDetails[] }; /** * The state of the ProductDetailProvider component which is passed to the wrapped component as a * prop. * * @template T The type of product data that will be provided. Defaults to `Product` */ export type WithProductDetailState< T extends CommerceTypes.Product = CommerceTypes.Product > = Pick, 'commerceData'> & { reviewsData?: ReviewTypes.ReviewDetails[] }; /** * A function that wraps a a component and returns a new high order component. The wrapped * component will be given product detail data as props. * * @template T The type of product data that will be provided. Defaults to `Product` * * @param {ComponentClass

} WrappedComponent A component to wrap and * provide product detail data to as props. * @returns {ComponentClass

} A high order component. */ export type ProductDetailWrapper = ( WrappedComponent: ComponentClass

> ) => ComponentClass

>; /** * Returns a function that wraps a component and returns a new high order component. The wrapped * component will be given product detail data as props. * * @template P The original props of the wrapped component. They'll be passed through unmodified. * @template T The type of product data that will be provided. Defaults to `Product` * * @param {FetchDataFunction} fetchProduct A function that will return product data. * @returns {ProductDetailWrapper

} A function that wraps a component and returns a new high order * component. */ export default function withProductDetailData< P, T extends CommerceTypes.Product = CommerceTypes.Product >(fetchProduct: FetchDataFunction): ProductDetailWrapper { type ResultProps = P & WithProductDetailProviderProps & WithCommerceProps; /** * A function that wraps a a component and returns a new high order component. The wrapped * component will be given product detail data as props. * * @param {ComponentClass

} WrappedComponent A component to wrap and * provide product detail data to as props. * @returns {ComponentClass

} A high order component. */ return (WrappedComponent: ComponentClass

>) => { class ProductDetailProvider extends Component> { async componentDidUpdate(prevProps: ResultProps): Promise { const { commerceToReviewMap, reviewDataSource } = this.props; // ts isn't detecting the commerceData type correctly, so we have to assert it const commerceData = this.props.commerceData as T | undefined; if (commerceData === undefined || reviewDataSource === undefined) { return; } if (!isEqual(prevProps.commerceData, commerceData)) { // CommerceData has changed, update review data const ids = reviewDataSource.productIdMapper( [commerceData], commerceToReviewMap ); const reviewsData = await reviewDataSource.fetchReviewDetails({ ids }); // Merge commerce and reviews data const newCommerceData = cloneDeep(commerceData); set(newCommerceData, 'review', reviewsData[0]); this.setState({ commerceData: newCommerceData, reviewsData }); } } render(): JSX.Element { const { commerceToReviewMap, ...props } = this.props as any; // TypeScript does not support rest parameters for generics :( return ( ); } } return compose>>( withCommerceData(fetchProduct) )(ProductDetailProvider); }; }