diff --git a/.changeset/fluffy-birds-battle.md b/.changeset/fluffy-birds-battle.md new file mode 100644 index 00000000..26abae28 --- /dev/null +++ b/.changeset/fluffy-birds-battle.md @@ -0,0 +1,8 @@ +--- +"@elasticpath/react-shopper-hooks": patch +"@elasticpath/composable-common": patch +"composable-cli": patch +"@elasticpath/d2c-schematics": patch +--- + +- bumped moltin sdk version diff --git a/.changeset/tasty-kiwis-change.md b/.changeset/tasty-kiwis-change.md new file mode 100644 index 00000000..ab8615b9 --- /dev/null +++ b/.changeset/tasty-kiwis-change.md @@ -0,0 +1,6 @@ +--- +"@elasticpath/d2c-schematics": minor +--- + +- Added bundles support to d2c output +- Refactored product components to use the latest react shopper hooks diff --git a/examples/algolia/package.json b/examples/algolia/package.json index 0c369c90..3afa00f8 100644 --- a/examples/algolia/package.json +++ b/examples/algolia/package.json @@ -24,7 +24,7 @@ "@elasticpath/react-shopper-hooks": "^0.3.2", "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.0.18", - "@moltin/sdk": "^23.3.0", + "@moltin/sdk": "^25.0.2", "algoliasearch": "^4.14.2", "clsx": "^1.2.1", "cookies-next": "^2.1.1", @@ -40,7 +40,9 @@ "react-instantsearch-hooks-server": "6.38.1", "react-instantsearch-hooks-web": "6.38.1", "react-toastify": "^9.1.3", - "yup": "^1.3.0" + "yup": "^1.3.0", + "zod": "^3.22.4", + "zod-formik-adapter": "^1.2.0" }, "devDependencies": { "@babel/core": "^7.18.10", diff --git a/examples/algolia/public/150-placeholder.png b/examples/algolia/public/150-placeholder.png new file mode 100644 index 00000000..61cd0d97 Binary files /dev/null and b/examples/algolia/public/150-placeholder.png differ diff --git a/examples/algolia/src/components/cart/Promotion.tsx b/examples/algolia/src/components/cart/Promotion.tsx index 81389128..19906809 100644 --- a/examples/algolia/src/components/cart/Promotion.tsx +++ b/examples/algolia/src/components/cart/Promotion.tsx @@ -12,7 +12,7 @@ export const Promotion = (): JSX.Element => { promoCode: "", }; - const { handleSubmit, handleChange, values, errors } = useFormik({ + const { handleSubmit, handleChange, values } = useFormik({ initialValues, onSubmit: async (values) => { await addPromotionToCart(values.promoCode); diff --git a/examples/algolia/src/components/header/cart/CartUpdatingSpinner.tsx b/examples/algolia/src/components/header/cart/CartUpdatingSpinner.tsx deleted file mode 100644 index fd1a5b8e..00000000 --- a/examples/algolia/src/components/header/cart/CartUpdatingSpinner.tsx +++ /dev/null @@ -1,24 +0,0 @@ -export default function CartUpdatingSpinner(): JSX.Element { - return ( - - - - - ); -} diff --git a/examples/algolia/src/components/product-modal/BaseProduct.tsx b/examples/algolia/src/components/product-modal/BaseProduct.tsx deleted file mode 100644 index 6d2fda90..00000000 --- a/examples/algolia/src/components/product-modal/BaseProduct.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { IBaseProduct } from "../../lib/types/product-types"; -import ProductVariations from "./ProductVariations"; -import ProductContainer from "./ProductContainer"; - -interface IBaseProductDetail { - baseProduct: IBaseProduct; -} - -const BaseProductDetail = ({ - baseProduct, -}: IBaseProductDetail): JSX.Element => { - const { - product: { attributes, id }, - variations, - variationsMatrix, - } = baseProduct; - return ( - - {variations && ( - - )} - - ); -}; - -export default BaseProductDetail; diff --git a/examples/algolia/src/components/product-modal/ChildProduct.tsx b/examples/algolia/src/components/product-modal/ChildProduct.tsx deleted file mode 100644 index f29d4d94..00000000 --- a/examples/algolia/src/components/product-modal/ChildProduct.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { IChildProduct } from "../../lib/types/product-types"; -import ProductVariations from "./ProductVariations"; -import ProductContainer from "./ProductContainer"; - -interface IChildProductDetail { - childProduct: IChildProduct; -} - -const ChildProductDetail = ({ - childProduct, -}: IChildProductDetail): JSX.Element => { - const { product, baseProduct, variations, variationsMatrix } = childProduct; - return ( - - {variations && ( - - )} - - ); -}; - -export default ChildProductDetail; diff --git a/examples/algolia/src/components/product-modal/Product.tsx b/examples/algolia/src/components/product-modal/Product.tsx index 01697e79..157cf22f 100644 --- a/examples/algolia/src/components/product-modal/Product.tsx +++ b/examples/algolia/src/components/product-modal/Product.tsx @@ -1,22 +1,22 @@ import type { NextPage } from "next"; import { ProductModalContext } from "../../lib/product-util"; import { useEffect, useState } from "react"; -import type { IProduct } from "../../lib/types/product-types"; -import BaseProductDetail from "./BaseProduct"; -import ChildProductDetail from "./ChildProduct"; -import SimpleProductDetail from "./SimpleProduct"; +import type { ShopperProduct } from "@elasticpath/react-shopper-hooks"; +import BundleProductDetail from "../product/bundles/BundleProduct"; +import SimpleProductDetail from "../product/SimpleProduct"; +import { VariationProductDetail } from "../product/variations/VariationProduct"; interface ModalProductProps { onSkuIdChange: (id: string) => void; } -export const Product: NextPage = ( - props: IProduct & ModalProductProps, +export const Product: NextPage = ( + props: ShopperProduct & ModalProductProps, ) => { const [isChangingSku, setIsChangingSku] = useState(false); const [changedSkuId, setChangedSkuId] = useState(""); - const { product } = props; + const { response } = props; useEffect(() => { if (changedSkuId && props.onSkuIdChange) { @@ -25,7 +25,7 @@ export const Product: NextPage = ( }, [changedSkuId, props]); return ( -
+
= ( ); }; -function resolveProductDetailComponent(props: IProduct): JSX.Element { - switch (props.kind) { +function resolveProductDetailComponent(product: ShopperProduct): JSX.Element { + switch (product.kind) { case "base-product": - return ; + return ; case "child-product": - return ; + return ; case "simple-product": - return ; + return ; + case "bundle-product": + return ; } } diff --git a/examples/algolia/src/components/product-modal/ProductComponents.tsx b/examples/algolia/src/components/product-modal/ProductComponents.tsx deleted file mode 100644 index aa67a641..00000000 --- a/examples/algolia/src/components/product-modal/ProductComponents.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Fragment } from "react"; -import { ProductResponse } from "@moltin/sdk"; - -interface IProductComponentsProps { - components: ProductResponse[]; - product: ProductResponse; -} - -const ProductComponents = ({ - components, - product, -}: IProductComponentsProps): JSX.Element => { - return ( -
- {Object.keys(product.attributes.components).map((cmpName) => { - const allOptions = product.attributes.components[cmpName].options; - const bundle_configuration = product.meta.bundle_configuration; - return ( -
- {cmpName} - - {bundle_configuration ? ( -
-
- {allOptions.map(({ id, quantity }, index) => { - const optionData = components.find( - (item) => item.id === id, - )!; - return ( - - - {allOptions.length > 1 && - index + 1 !== allOptions.length ? ( -
- ) : null} -
- ); - })} -
-
- ) : null} -
- ); - })} -
- ); -}; - -export default ProductComponents; diff --git a/examples/algolia/src/components/product-modal/ProductContainer.tsx b/examples/algolia/src/components/product-modal/ProductContainer.tsx deleted file mode 100644 index a9528ee3..00000000 --- a/examples/algolia/src/components/product-modal/ProductContainer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import ProductSummary from "./ProductSummary"; -import CartActions from "../product/CartActions"; -import { IBase } from "../../lib/types/product-types"; -import { ReactElement } from "react"; -import ProductDetails from "../product/ProductDetails"; -import { EyeSlashIcon } from "@heroicons/react/24/solid"; -import Image from "next/image"; -import Link from "next/link"; - -interface IProductContainer { - productBase: IBase; - children?: ReactElement; -} - -export default function ProductContainer({ - productBase: { product, main_image }, - children, -}: IProductContainer): JSX.Element { - return ( -
- {main_image ? ( - {product.attributes.name} - ) : ( -
- -
- )} -
- - - {children} -
- - - - View full details - - -
-
-
- ); -} diff --git a/examples/algolia/src/components/product-modal/ProductSummary.tsx b/examples/algolia/src/components/product-modal/ProductSummary.tsx deleted file mode 100644 index 0dd95d6d..00000000 --- a/examples/algolia/src/components/product-modal/ProductSummary.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { ProductResponse } from "@moltin/sdk"; -import clsx from "clsx"; -import { useContext } from "react"; -import { ProductContext } from "../../lib/product-util"; -import Price from "../product/Price"; -import StrikePrice from "../product/StrikePrice"; - -interface IProductSummary { - product: ProductResponse; -} - -const ProductSummary = ({ product }: IProductSummary): JSX.Element => { - const { - attributes, - meta: { display_price, original_display_price }, - } = product; - const context = useContext(ProductContext); - - return ( -
- {attributes.name} - {display_price && ( -
- - {original_display_price && ( - - )} -
- )} -
- ); -}; - -export default ProductSummary; diff --git a/examples/algolia/src/components/product-modal/ProductVariations.tsx b/examples/algolia/src/components/product-modal/ProductVariations.tsx deleted file mode 100644 index 1f06faa3..00000000 --- a/examples/algolia/src/components/product-modal/ProductVariations.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type { CatalogsProductVariation } from "@moltin/sdk"; -import { useRouter } from "next/router"; -import { useContext } from "react"; -import { useEffect, useState } from "react"; -import { OptionDict } from "../../lib/types/product-types"; -import { - createEmptyOptionDict, - ProductModalContext, -} from "../../lib/product-util"; -import { - allVariationsHaveSelectedOption, - getOptionsFromSkuId, - getSkuIdFromOptions, - mapOptionsToVariation, -} from "../../lib/product-helper"; -import ProductVariationStandard, { - UpdateOptionHandler, -} from "../product/variations/ProductVariationStandard"; -import { MatrixObjectEntry } from "../../lib/types/matrix-object-entry"; - -interface IProductVariations { - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; - baseProductSlug: string; - currentSkuId: string; - skuOptions?: string[]; -} - -const getSelectedOption = ( - variationId: string, - optionLookupObj: OptionDict, -): string => { - return optionLookupObj[variationId]; -}; - -const ProductVariations = ({ - variations, - baseProductSlug, - currentSkuId, - variationsMatrix, -}: IProductVariations): JSX.Element => { - const currentSkuOptions = getOptionsFromSkuId(currentSkuId, variationsMatrix); - const initialOptions = currentSkuOptions - ? mapOptionsToVariation(currentSkuOptions, variations) - : createEmptyOptionDict(variations); - - const context = useContext(ProductModalContext); - const [selectedOptions, setSelectedOptions] = - useState(initialOptions); - const router = useRouter(); - - useEffect(() => { - const selectedSkuId = getSkuIdFromOptions( - Object.values(selectedOptions), - variationsMatrix, - ); - - if ( - selectedSkuId && - selectedSkuId !== currentSkuId && - allVariationsHaveSelectedOption(selectedOptions, variations) - ) { - context?.setChangedSkuId(selectedSkuId); - } - }, [ - selectedOptions, - baseProductSlug, - context, - currentSkuId, - router, - variations, - variationsMatrix, - ]); - - const updateOptionHandler: UpdateOptionHandler = - (variationId) => - (optionId): void => { - for (const selectedOptionKey in selectedOptions) { - if (selectedOptionKey === variationId) { - setSelectedOptions({ - ...selectedOptions, - [selectedOptionKey]: optionId, - }); - break; - } - } - }; - - return ( -
- {variations.map((v) => ( - - ))} -
- ); -}; - -export default ProductVariations; diff --git a/examples/algolia/src/components/product-modal/SimpleProduct.tsx b/examples/algolia/src/components/product-modal/SimpleProduct.tsx deleted file mode 100644 index 8c80e8db..00000000 --- a/examples/algolia/src/components/product-modal/SimpleProduct.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ISimpleProduct } from "../../lib/types/product-types"; -import ProductComponents from "./ProductComponents"; -import ProductContainer from "./ProductContainer"; - -interface ISimpleProductDetail { - simpleProduct: ISimpleProduct; -} - -const SimpleProductDetail = ({ - simpleProduct, -}: ISimpleProductDetail): JSX.Element => { - const { product, component_products } = simpleProduct; - return ( - - {component_products && ( - - )} - - ); -}; - -export default SimpleProductDetail; diff --git a/examples/algolia/src/components/product/BaseProduct.tsx b/examples/algolia/src/components/product/BaseProduct.tsx deleted file mode 100644 index 6d2fda90..00000000 --- a/examples/algolia/src/components/product/BaseProduct.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { IBaseProduct } from "../../lib/types/product-types"; -import ProductVariations from "./ProductVariations"; -import ProductContainer from "./ProductContainer"; - -interface IBaseProductDetail { - baseProduct: IBaseProduct; -} - -const BaseProductDetail = ({ - baseProduct, -}: IBaseProductDetail): JSX.Element => { - const { - product: { attributes, id }, - variations, - variationsMatrix, - } = baseProduct; - return ( - - {variations && ( - - )} - - ); -}; - -export default BaseProductDetail; diff --git a/examples/algolia/src/components/product/CartActions.tsx b/examples/algolia/src/components/product/CartActions.tsx index c303c75a..5429bfc2 100644 --- a/examples/algolia/src/components/product/CartActions.tsx +++ b/examples/algolia/src/components/product/CartActions.tsx @@ -4,13 +4,9 @@ import { useCart } from "@elasticpath/react-shopper-hooks"; import Spinner from "../Spinner"; import clsx from "clsx"; -interface ICartActions { - productId: string; -} - -const CartActions = ({ productId }: ICartActions): JSX.Element => { +const CartActions = (): JSX.Element => { const context = useContext(ProductContext); - const { addProductToCart, isUpdatingCart } = useCart(); + const { isUpdatingCart } = useCart(); return (
{ >
))} diff --git a/examples/algolia/src/components/product/variations/ProductVariationStandard.tsx b/examples/algolia/src/components/product/variations/ProductVariationStandard.tsx index a9315791..a326191c 100644 --- a/examples/algolia/src/components/product/variations/ProductVariationStandard.tsx +++ b/examples/algolia/src/components/product/variations/ProductVariationStandard.tsx @@ -1,4 +1,5 @@ import clsx from "clsx"; +import type { useVariationProduct } from "@elasticpath/react-shopper-hooks"; interface ProductVariationOption { id: string; @@ -16,7 +17,9 @@ interface IProductVariation { name: string; options: ProductVariationOption[]; }; - updateOptionHandler: UpdateOptionHandler; + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"]; selectedOptionId?: string; } @@ -31,6 +34,7 @@ const ProductVariationStandard = ({
{variation.options.map((o) => ( diff --git a/examples/algolia/src/components/product/ProductVariations.tsx b/examples/algolia/src/components/product/variations/ProductVariations.tsx similarity index 53% rename from examples/algolia/src/components/product/ProductVariations.tsx rename to examples/algolia/src/components/product/variations/ProductVariations.tsx index 7332a23c..f1c4f524 100644 --- a/examples/algolia/src/components/product/ProductVariations.tsx +++ b/examples/algolia/src/components/product/variations/ProductVariations.tsx @@ -1,28 +1,16 @@ import type { CatalogsProductVariation } from "@moltin/sdk"; import { useRouter } from "next/router"; import { useContext } from "react"; -import { useEffect, useState } from "react"; -import { OptionDict } from "../../lib/types/product-types"; -import { createEmptyOptionDict, ProductContext } from "../../lib/product-util"; +import { useEffect } from "react"; +import { OptionDict } from "../../../lib/types/product-types"; +import { ProductContext } from "../../../lib/product-util"; import { allVariationsHaveSelectedOption, - getOptionsFromSkuId, getSkuIdFromOptions, - mapOptionsToVariation, -} from "../../lib/product-helper"; -import ProductVariationStandard, { - UpdateOptionHandler, -} from "./variations/ProductVariationStandard"; -import ProductVariationColor from "./variations/ProductVariationColor"; -import { MatrixObjectEntry } from "../../lib/types/matrix-object-entry"; - -interface IProductVariations { - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; - baseProductSlug: string; - currentSkuId: string; - skuOptions?: string[]; -} +} from "../../../lib/product-helper"; +import ProductVariationStandard from "./ProductVariationStandard"; +import ProductVariationColor from "./ProductVariationColor"; +import { useVariationProduct } from "@elasticpath/react-shopper-hooks"; const getSelectedOption = ( variationId: string, @@ -31,20 +19,19 @@ const getSelectedOption = ( return optionLookupObj[variationId]; }; -const ProductVariations = ({ - variations, - baseProductSlug, - currentSkuId, - variationsMatrix, -}: IProductVariations): JSX.Element => { - const currentSkuOptions = getOptionsFromSkuId(currentSkuId, variationsMatrix); - const initialOptions = currentSkuOptions - ? mapOptionsToVariation(currentSkuOptions, variations) - : createEmptyOptionDict(variations); +const ProductVariations = (): JSX.Element => { + const { + variations, + variationsMatrix, + product, + selectedOptions, + updateSelectedOptions, + } = useVariationProduct(); + + const currentProductId = product.response.id; const context = useContext(ProductContext); - const [selectedOptions, setSelectedOptions] = - useState(initialOptions); + const router = useRouter(); useEffect(() => { @@ -56,7 +43,7 @@ const ProductVariations = ({ if ( !context?.isChangingSku && selectedSkuId && - selectedSkuId !== currentSkuId && + selectedSkuId !== currentProductId && allVariationsHaveSelectedOption(selectedOptions, variations) ) { context?.setIsChangingSku(true); @@ -66,28 +53,13 @@ const ProductVariations = ({ } }, [ selectedOptions, - baseProductSlug, context, - currentSkuId, + currentProductId, router, variations, variationsMatrix, ]); - const updateOptionHandler: UpdateOptionHandler = - (variationId) => - (optionId): void => { - for (const selectedOptionKey in selectedOptions) { - if (selectedOptionKey === variationId) { - setSelectedOptions({ - ...selectedOptions, - [selectedOptionKey]: optionId, - }); - break; - } - } - }; - return (
resolveVariationComponentByName( v, - updateOptionHandler, + updateSelectedOptions, getSelectedOption(v.id, selectedOptions), ), )} @@ -107,7 +79,9 @@ const ProductVariations = ({ function resolveVariationComponentByName( v: CatalogsProductVariation, - updateOptionHandler: UpdateOptionHandler, + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"], selectedOptionId?: string, ): JSX.Element { switch (v.name.toLowerCase()) { diff --git a/examples/algolia/src/components/product/variations/VariationProduct.tsx b/examples/algolia/src/components/product/variations/VariationProduct.tsx new file mode 100644 index 00000000..23d30a85 --- /dev/null +++ b/examples/algolia/src/components/product/variations/VariationProduct.tsx @@ -0,0 +1,43 @@ +import { + useCart, + useVariationProduct, + VariationProduct, + VariationProductProvider, +} from "@elasticpath/react-shopper-hooks"; +import { useCallback } from "react"; +import { Formik } from "formik"; +import ProductContainer from "../ProductContainer"; +import ProductVariations from "./ProductVariations"; + +export const VariationProductDetail = ({ + variationProduct, +}: { + variationProduct: VariationProduct; +}): JSX.Element => { + return ( + + + + ); +}; + +export function VariationProductContainer(): JSX.Element { + const { addProductToCart } = useCart(); + const { product } = useVariationProduct(); + + const { + response: { id }, + } = product; + + const submit = useCallback(async () => { + await addProductToCart(id, 1); + }, [id, addProductToCart]); + + return ( + submit()}> + + + + + ); +} diff --git a/examples/algolia/src/components/promotion-banner/PromotionBanner.tsx b/examples/algolia/src/components/promotion-banner/PromotionBanner.tsx index 84b45f4a..a7be243d 100644 --- a/examples/algolia/src/components/promotion-banner/PromotionBanner.tsx +++ b/examples/algolia/src/components/promotion-banner/PromotionBanner.tsx @@ -18,7 +18,7 @@ interface IPromotionBanner { const PromotionBanner = (props: IPromotionBanner): JSX.Element => { const router = useRouter(); - const { linkProps, promotion, alignment } = props; + const { linkProps, promotion } = props; const { title, description } = promotion; diff --git a/examples/algolia/src/components/search/Hit.tsx b/examples/algolia/src/components/search/Hit.tsx index a439e140..e83afd2b 100644 --- a/examples/algolia/src/components/search/Hit.tsx +++ b/examples/algolia/src/components/search/Hit.tsx @@ -8,18 +8,12 @@ import { Dialog, Transition } from "@headlessui/react"; import { Fragment, useEffect, useState } from "react"; import Product from "../product-modal/Product"; import { getProductById } from "../../services/products"; -import { - isChildProductResource, - isSimpleProductResource, -} from "../../lib/product-helper"; -import { - retrieveBaseProps, - retrieveChildProps, - retrieveSimpleProps, -} from "../../lib/retrieve-product-props"; -import { IProduct } from "../../lib/types/product-types"; -import { GetStaticPropsResult } from "next/types"; import { EyeSlashIcon, XMarkIcon } from "@heroicons/react/24/solid"; +import { + parseProductResponse, + ShopperProduct, +} from "@elasticpath/react-shopper-hooks"; +import { getEpccImplicitClient } from "../../lib/epcc-implicit-client"; export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element { const { ep_price, ep_name, objectID, ep_main_image_url, ep_description } = @@ -38,17 +32,14 @@ export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element { const fetchProduct = async (id: string) => { const product = await getProductById(id); - const productData = product.data; - const retrievedResults = isSimpleProductResource(productData) - ? retrieveSimpleProps(product) - : isChildProductResource(productData) - ? await retrieveChildProps(product) - : await retrieveBaseProps(product); - setProductProps(retrievedResults); + const retrievedResults = await parseProductResponse( + product, + getEpccImplicitClient(), + ); + setProduct(retrievedResults); }; - const [productProps, setProductProps] = - useState>(); + const [product, setProduct] = useState(); useEffect(() => { isOpen && fetchProduct(objectID); @@ -112,7 +103,8 @@ export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element {
))} diff --git a/examples/basic/src/components/product/variations/ProductVariationStandard.tsx b/examples/basic/src/components/product/variations/ProductVariationStandard.tsx index a9315791..a326191c 100644 --- a/examples/basic/src/components/product/variations/ProductVariationStandard.tsx +++ b/examples/basic/src/components/product/variations/ProductVariationStandard.tsx @@ -1,4 +1,5 @@ import clsx from "clsx"; +import type { useVariationProduct } from "@elasticpath/react-shopper-hooks"; interface ProductVariationOption { id: string; @@ -16,7 +17,9 @@ interface IProductVariation { name: string; options: ProductVariationOption[]; }; - updateOptionHandler: UpdateOptionHandler; + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"]; selectedOptionId?: string; } @@ -31,6 +34,7 @@ const ProductVariationStandard = ({
{variation.options.map((o) => ( diff --git a/examples/basic/src/components/product/ProductVariations.tsx b/examples/basic/src/components/product/variations/ProductVariations.tsx similarity index 53% rename from examples/basic/src/components/product/ProductVariations.tsx rename to examples/basic/src/components/product/variations/ProductVariations.tsx index 7332a23c..f1c4f524 100644 --- a/examples/basic/src/components/product/ProductVariations.tsx +++ b/examples/basic/src/components/product/variations/ProductVariations.tsx @@ -1,28 +1,16 @@ import type { CatalogsProductVariation } from "@moltin/sdk"; import { useRouter } from "next/router"; import { useContext } from "react"; -import { useEffect, useState } from "react"; -import { OptionDict } from "../../lib/types/product-types"; -import { createEmptyOptionDict, ProductContext } from "../../lib/product-util"; +import { useEffect } from "react"; +import { OptionDict } from "../../../lib/types/product-types"; +import { ProductContext } from "../../../lib/product-util"; import { allVariationsHaveSelectedOption, - getOptionsFromSkuId, getSkuIdFromOptions, - mapOptionsToVariation, -} from "../../lib/product-helper"; -import ProductVariationStandard, { - UpdateOptionHandler, -} from "./variations/ProductVariationStandard"; -import ProductVariationColor from "./variations/ProductVariationColor"; -import { MatrixObjectEntry } from "../../lib/types/matrix-object-entry"; - -interface IProductVariations { - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; - baseProductSlug: string; - currentSkuId: string; - skuOptions?: string[]; -} +} from "../../../lib/product-helper"; +import ProductVariationStandard from "./ProductVariationStandard"; +import ProductVariationColor from "./ProductVariationColor"; +import { useVariationProduct } from "@elasticpath/react-shopper-hooks"; const getSelectedOption = ( variationId: string, @@ -31,20 +19,19 @@ const getSelectedOption = ( return optionLookupObj[variationId]; }; -const ProductVariations = ({ - variations, - baseProductSlug, - currentSkuId, - variationsMatrix, -}: IProductVariations): JSX.Element => { - const currentSkuOptions = getOptionsFromSkuId(currentSkuId, variationsMatrix); - const initialOptions = currentSkuOptions - ? mapOptionsToVariation(currentSkuOptions, variations) - : createEmptyOptionDict(variations); +const ProductVariations = (): JSX.Element => { + const { + variations, + variationsMatrix, + product, + selectedOptions, + updateSelectedOptions, + } = useVariationProduct(); + + const currentProductId = product.response.id; const context = useContext(ProductContext); - const [selectedOptions, setSelectedOptions] = - useState(initialOptions); + const router = useRouter(); useEffect(() => { @@ -56,7 +43,7 @@ const ProductVariations = ({ if ( !context?.isChangingSku && selectedSkuId && - selectedSkuId !== currentSkuId && + selectedSkuId !== currentProductId && allVariationsHaveSelectedOption(selectedOptions, variations) ) { context?.setIsChangingSku(true); @@ -66,28 +53,13 @@ const ProductVariations = ({ } }, [ selectedOptions, - baseProductSlug, context, - currentSkuId, + currentProductId, router, variations, variationsMatrix, ]); - const updateOptionHandler: UpdateOptionHandler = - (variationId) => - (optionId): void => { - for (const selectedOptionKey in selectedOptions) { - if (selectedOptionKey === variationId) { - setSelectedOptions({ - ...selectedOptions, - [selectedOptionKey]: optionId, - }); - break; - } - } - }; - return (
resolveVariationComponentByName( v, - updateOptionHandler, + updateSelectedOptions, getSelectedOption(v.id, selectedOptions), ), )} @@ -107,7 +79,9 @@ const ProductVariations = ({ function resolveVariationComponentByName( v: CatalogsProductVariation, - updateOptionHandler: UpdateOptionHandler, + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"], selectedOptionId?: string, ): JSX.Element { switch (v.name.toLowerCase()) { diff --git a/examples/basic/src/components/product/variations/VariationProduct.tsx b/examples/basic/src/components/product/variations/VariationProduct.tsx new file mode 100644 index 00000000..23d30a85 --- /dev/null +++ b/examples/basic/src/components/product/variations/VariationProduct.tsx @@ -0,0 +1,43 @@ +import { + useCart, + useVariationProduct, + VariationProduct, + VariationProductProvider, +} from "@elasticpath/react-shopper-hooks"; +import { useCallback } from "react"; +import { Formik } from "formik"; +import ProductContainer from "../ProductContainer"; +import ProductVariations from "./ProductVariations"; + +export const VariationProductDetail = ({ + variationProduct, +}: { + variationProduct: VariationProduct; +}): JSX.Element => { + return ( + + + + ); +}; + +export function VariationProductContainer(): JSX.Element { + const { addProductToCart } = useCart(); + const { product } = useVariationProduct(); + + const { + response: { id }, + } = product; + + const submit = useCallback(async () => { + await addProductToCart(id, 1); + }, [id, addProductToCart]); + + return ( + submit()}> + + + + + ); +} diff --git a/examples/basic/src/components/promotion-banner/PromotionBanner.tsx b/examples/basic/src/components/promotion-banner/PromotionBanner.tsx index 84b45f4a..a7be243d 100644 --- a/examples/basic/src/components/promotion-banner/PromotionBanner.tsx +++ b/examples/basic/src/components/promotion-banner/PromotionBanner.tsx @@ -18,7 +18,7 @@ interface IPromotionBanner { const PromotionBanner = (props: IPromotionBanner): JSX.Element => { const router = useRouter(); - const { linkProps, promotion, alignment } = props; + const { linkProps, promotion } = props; const { title, description } = promotion; diff --git a/examples/basic/src/lib/menu-style.ts b/examples/basic/src/lib/menu-style.ts deleted file mode 100644 index 7d0e31ae..00000000 --- a/examples/basic/src/lib/menu-style.ts +++ /dev/null @@ -1,11 +0,0 @@ -const menuItemInteractionStyle = { - bg: "none", - color: "brand.primary", -}; - -export const menuItemStyleProps = { - _hover: menuItemInteractionStyle, - _active: menuItemInteractionStyle, - _focus: menuItemInteractionStyle, - color: "gray.500", -}; diff --git a/examples/basic/src/lib/product-util.test.ts b/examples/basic/src/lib/product-util.test.ts index 0e301a03..d1882706 100644 --- a/examples/basic/src/lib/product-util.test.ts +++ b/examples/basic/src/lib/product-util.test.ts @@ -10,7 +10,6 @@ import { excludeChildProducts, filterBaseProducts, getProductMainImage, - getProductOtherImageUrls, processImageFiles, } from "./product-util"; @@ -134,47 +133,6 @@ describe("product util", () => { expect(processImageFiles(files as File[])).toEqual(expected); }); - test("getProductOtherImageUrls should return a products images files not including the main image", () => { - const files: Partial[] = [ - { - type: "file", - id: "123", - mime_type: "image/jpeg", - }, - { - type: "file", - id: "456", - mime_type: "image/jpeg", - }, - ]; - - const mainImageFile: Partial = { - type: "file", - id: "123", - mime_type: "image/jpeg", - }; - - const productResp: Partial> = { - included: { - files: files as File[], - main_images: [mainImageFile] as File[], - }, - }; - - const expected: Partial[] = [ - { - type: "file", - id: "456", - mime_type: "image/jpeg", - }, - ]; - expect( - getProductOtherImageUrls( - productResp as ShopperCatalogResource, - ), - ).toEqual(expected); - }); - test("getProductMainImage should return a products main image file", () => { const mainImageFile: Partial = { type: "file", @@ -188,11 +146,9 @@ describe("product util", () => { }, }; - expect( - getProductMainImage( - productResp as ShopperCatalogResource, - ), - ).toEqual(mainImageFile); + expect(getProductMainImage(productResp.included?.main_images)).toEqual( + mainImageFile, + ); }); test("getProductMainImage should return null when product does not have main image included", () => { @@ -200,11 +156,9 @@ describe("product util", () => { included: {}, }; - expect( - getProductMainImage( - productResp as ShopperCatalogResource, - ), - ).toEqual(null); + expect(getProductMainImage(productResp.included?.main_images)).toEqual( + null, + ); }); test("createEmptyOptionDict should return an OptionDict with all with variation keys assigned undefined values", () => { diff --git a/examples/basic/src/lib/product-util.ts b/examples/basic/src/lib/product-util.ts index 63f78260..fa9a0eb6 100644 --- a/examples/basic/src/lib/product-util.ts +++ b/examples/basic/src/lib/product-util.ts @@ -2,7 +2,6 @@ import type { File, ProductResponse, CatalogsProductVariation, - ShopperCatalogResource, } from "@moltin/sdk"; import { createContext } from "react"; import type { @@ -30,19 +29,10 @@ export function processImageFiles(files: File[], mainImageId?: string) { ); } -export function getProductOtherImageUrls( - productResp: ShopperCatalogResource, -): File[] { - const files = productResp?.included?.files; - return files - ? processImageFiles(files, productResp?.included?.main_images?.[0].id) - : []; -} - export function getProductMainImage( - productResp: ShopperCatalogResource, + mainImages: File[] | undefined, ): File | null { - return productResp?.included?.main_images?.[0] || null; + return mainImages?.[0] || null; } // Using existance of parent relationship property to filter because only child products seem to have this property. diff --git a/examples/basic/src/lib/retrieve-product-props.ts b/examples/basic/src/lib/retrieve-product-props.ts deleted file mode 100644 index 7ed550f8..00000000 --- a/examples/basic/src/lib/retrieve-product-props.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ProductResponse, Resource, ShopperCatalogResource } from "@moltin/sdk"; -import { GetStaticPropsResult } from "next"; -import { - IBaseProduct, - IChildProduct, - ISimpleProduct, -} from "./types/product-types"; -import { getProductMainImage, getProductOtherImageUrls } from "./product-util"; -import { getProductById } from "../services/products"; -import { sortAlphabetically } from "./sort-alphabetically"; - -export function retrieveSimpleProps( - productResource: ShopperCatalogResource, -): GetStaticPropsResult { - const component_products = productResource.included?.component_products; - return { - props: { - kind: "simple-product", - product: productResource.data, - main_image: getProductMainImage(productResource), - otherImages: getProductOtherImageUrls(productResource), - ...(component_products && { component_products }), - }, - }; -} - -export async function retrieveChildProps( - childProductResource: Resource, -): Promise> { - const baseProductId = childProductResource.data.attributes.base_product_id; - const baseProduct = await getProductById(baseProductId); - if (!baseProduct) { - throw Error( - `Unable to retrieve child props, failed to get the base product for ${baseProductId}`, - ); - } - const { - data: { - meta: { variation_matrix, variations }, - }, - } = baseProduct; - - if (!variations || !variation_matrix) { - throw Error( - `Unable to retrieve child props, failed to get the variations or variation_matrix from base product for ${baseProductId}`, - ); - } - - return { - props: { - kind: "child-product", - product: childProductResource.data, - baseProduct: baseProduct.data, - main_image: getProductMainImage(childProductResource), - otherImages: getProductOtherImageUrls(childProductResource), - variationsMatrix: variation_matrix, - variations: variations.sort(sortAlphabetically), - }, - }; -} - -export async function retrieveBaseProps( - baseProductResource: Resource, -): Promise> { - const { - data: { - meta: { variations, variation_matrix }, - attributes: { slug }, - }, - } = baseProductResource; - - if (!variations || !variation_matrix) { - throw Error( - `Unable to retrieve base product props, failed to get the variations or variation_matrix from base product for ${slug}`, - ); - } - - return { - props: { - kind: "base-product", - product: baseProductResource.data, - main_image: getProductMainImage(baseProductResource), - otherImages: getProductOtherImageUrls(baseProductResource), - variationsMatrix: variation_matrix, - variations: variations.sort(sortAlphabetically), - }, - }; -} diff --git a/examples/basic/src/lib/types/deep-partial.ts b/examples/basic/src/lib/types/deep-partial.ts new file mode 100644 index 00000000..e422dd51 --- /dev/null +++ b/examples/basic/src/lib/types/deep-partial.ts @@ -0,0 +1,3 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; diff --git a/examples/basic/src/lib/types/product-types.ts b/examples/basic/src/lib/types/product-types.ts index ecbfa38a..a18a260d 100644 --- a/examples/basic/src/lib/types/product-types.ts +++ b/examples/basic/src/lib/types/product-types.ts @@ -1,42 +1,11 @@ -import type { - ProductResponse, - CatalogsProductVariation, - File, -} from "@moltin/sdk"; +import type { ProductResponse, File } from "@moltin/sdk"; import type { Dispatch, SetStateAction } from "react"; -import { MatrixObjectEntry } from "./matrix-object-entry"; export type IdentifiableBaseProduct = ProductResponse & { id: string; attributes: { slug: string; sku: string; base_product: true }; }; -export interface IBase { - product: ProductResponse; - main_image: File | null; - otherImages: File[]; - component_products?: ProductResponse[]; -} - -export interface IBaseProduct extends IBase { - kind: "base-product"; - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; -} - -export interface IChildProduct extends IBase { - kind: "child-product"; - baseProduct: ProductResponse; - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; -} - -export interface ISimpleProduct extends IBase { - kind: "simple-product"; -} - -export type IProduct = IBaseProduct | IChildProduct | ISimpleProduct; - export interface ProductContextState { isChangingSku: boolean; setIsChangingSku: Dispatch>; diff --git a/examples/basic/src/pages/products/[productId].tsx b/examples/basic/src/pages/products/[productId].tsx index 073bdf8e..07f50972 100644 --- a/examples/basic/src/pages/products/[productId].tsx +++ b/examples/basic/src/pages/products/[productId].tsx @@ -1,30 +1,28 @@ import type { GetStaticPaths } from "next"; import { ParsedUrlQuery } from "querystring"; -import ChildProductDetail from "../../components/product/ChildProduct"; -import BaseProductDetail from "../../components/product/BaseProduct"; -import { - isChildProductResource, - isSimpleProductResource, -} from "../../lib/product-helper"; + import { getProductById } from "../../services/products"; import SimpleProductDetail from "../../components/product/SimpleProduct"; import { ProductContext } from "../../lib/product-util"; import React, { ReactElement, useState } from "react"; -import type { IProduct } from "../../lib/types/product-types"; +import type { ShopperProduct } from "@elasticpath/react-shopper-hooks"; import { withStoreStaticProps } from "../../lib/store-wrapper-ssg"; -import { - retrieveBaseProps, - retrieveChildProps, - retrieveSimpleProps, -} from "../../lib/retrieve-product-props"; import Head from "next/head"; import { NextPageWithLayout } from "../_app"; import MainLayout, { MAIN_LAYOUT_TITLE, } from "../../components/layouts/MainLayout"; +import BundleProductDetail from "../../components/product/bundles/BundleProduct"; +import { parseProductResponse } from "@elasticpath/react-shopper-hooks"; +import { getEpccImplicitClient } from "../../lib/epcc-implicit-client"; +import { VariationProductDetail } from "../../components/product/variations/VariationProduct"; + +interface ProductPageProps { + product: ShopperProduct; +} -export const Product: NextPageWithLayout = (props: IProduct) => { +export const Product: NextPageWithLayout = (props) => { const [isChangingSku, setIsChangingSku] = useState(false); const { product } = props; @@ -32,7 +30,7 @@ export const Product: NextPageWithLayout = (props: IProduct) => { return (
= (props: IProduct) => { setIsChangingSku, }} > - {resolveProductDetailComponent(props)} + {resolveProductDetailComponent(product)}
); }; Product.getLayout = function getLayout(page: ReactElement, pageProps, ctx?) { + const { + attributes: { name, description }, + } = pageProps.product.response; + return ( <> {page} - - {`${MAIN_LAYOUT_TITLE} - ${pageProps.product.attributes.name}`} - - - - + {`${MAIN_LAYOUT_TITLE} - ${name}`} + + + ); }; -function resolveProductDetailComponent(props: IProduct): JSX.Element { - switch (props.kind) { +function resolveProductDetailComponent(product: ShopperProduct): JSX.Element { + switch (product.kind) { case "base-product": - return ; + return ; case "child-product": - return ; + return ; case "simple-product": - return ; + return ; + case "bundle-product": + return ; } } @@ -87,7 +80,7 @@ interface ProductRouteParams extends ParsedUrlQuery { } export const getStaticProps = withStoreStaticProps< - IProduct, + ProductPageProps, ProductRouteParams >(async ({ params }) => { if (!params) { @@ -104,16 +97,15 @@ export const getStaticProps = withStoreStaticProps< }; } - const productData = product.data; - - const retrievedResults = isSimpleProductResource(productData) - ? retrieveSimpleProps(product) - : isChildProductResource(productData) - ? await retrieveChildProps(product) - : await retrieveBaseProps(product); + const shopperProduct = await parseProductResponse( + product, + getEpccImplicitClient(), + ); return { - ...retrievedResults, + props: { + product: shopperProduct, + }, revalidate: 60, }; }); diff --git a/examples/basic/src/services/cart.ts b/examples/basic/src/services/cart.ts index 49a08498..dd15ad42 100644 --- a/examples/basic/src/services/cart.ts +++ b/examples/basic/src/services/cart.ts @@ -1,52 +1,7 @@ -import type { CartItemsResponse, Moltin as EPCCClient } from "@moltin/sdk"; +import type { Moltin as EPCCClient } from "@moltin/sdk"; import { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk"; import { getEpccImplicitClient } from "../lib/epcc-implicit-client"; -export async function removeCartItem( - id: string, - itemId: string, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()).Cart(id).RemoveItem(itemId); -} - -export async function removeAllCartItems( - id: string, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()).Cart(id).RemoveAllItems(); -} - -export async function updateCartItem( - id: string, - productId: string, - quantity: number, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()) - .Cart(id) - .UpdateItem(productId, quantity); -} - -export async function addPromotion( - id: string, - promoCode: string, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()).Cart(id).AddPromotion(promoCode); -} - -export async function addProductToCart( - cartId: string, - productId: string, - quantity: number, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()) - .Cart(cartId) - .AddProduct(productId, quantity); -} - export interface CustomItemRequest { type: "custom_item"; name: string; @@ -60,16 +15,6 @@ export interface CustomItemRequest { custom_inputs?: Record; } -export async function addCustomItemToCart( - cartId: string, - customItem: CustomItemRequest, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()) - .Cart(cartId) - .AddCustomItem(customItem); -} - export async function getCart( cartId: string, client?: EPCCClient, diff --git a/examples/basic/src/services/hierarchy.ts b/examples/basic/src/services/hierarchy.ts index 2418d5db..99ea9279 100644 --- a/examples/basic/src/services/hierarchy.ts +++ b/examples/basic/src/services/hierarchy.ts @@ -1,7 +1,6 @@ -import type { Node, Hierarchy, ProductResponse } from "@moltin/sdk"; +import type { Node, Hierarchy } from "@moltin/sdk"; import { Moltin as EPCCClient } from "@moltin/sdk"; -import { ShopperCatalogResourcePage } from "@moltin/sdk"; import { getEpccImplicitClient } from "../lib/epcc-implicit-client"; export async function getHierarchies( @@ -37,24 +36,3 @@ export async function getHierarchyNodes( return result.data; } - -export async function getNodeChildren( - nodeId: string, - client?: EPCCClient, -): Promise { - const result = await ( - client ?? getEpccImplicitClient() - ).ShopperCatalog.Nodes.GetNodeChildren({ - nodeId, - }); - return result.data; -} - -export async function getProductsByNode( - nodeId: string, - client?: EPCCClient, -): Promise> { - return await (client ?? getEpccImplicitClient()).ShopperCatalog.Products.With( - ["main_image", "files"], - ).GetProductsByNode({ nodeId }); -} diff --git a/package.json b/package.json index a7e6efac..26fad781 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "dotenv-cli": "^7.1.0", "eslint-config-custom": "*", "kebab-case": "^1.0.2", - "mkdirp": "^2.1.5", + "mkdirp": "2.1.5", "prettier": "latest", "quicktype-core": "^6.1.0", "ts-node": "^10.9.1", @@ -45,10 +45,13 @@ }, "engines": { "npm": ">=7.0.0", - "node": ">=14.0.0" + "node": ">=14.0.0 <18.0.0" }, "dependencies": { "@moltin/sdk": "^23.3.0" }, - "packageManager": "yarn@1.22.18" + "packageManager": "yarn@1.22.18", + "volta": { + "node": "16.20.2" + } } diff --git a/packages/composable-cli/package.json b/packages/composable-cli/package.json index 6c155d01..7aa79801 100644 --- a/packages/composable-cli/package.json +++ b/packages/composable-cli/package.json @@ -22,7 +22,7 @@ "@angular-devkit/core": "^14.1.0", "@angular-devkit/schematics": "^14.1.0", "@elasticpath/d2c-schematics": "*", - "@moltin/sdk": "^23.2.0", + "@moltin/sdk": "^25.0.2", "@types/ink": "^2.0.3", "@types/yargs": "^17.0.24", "algoliasearch": "^4.20.0", diff --git a/packages/composable-common/package.json b/packages/composable-common/package.json index 956d7709..0ceb0f66 100644 --- a/packages/composable-common/package.json +++ b/packages/composable-common/package.json @@ -45,7 +45,7 @@ "dependencies": { "@angular-devkit/core": "^15.2.2", "@angular-devkit/schematics": "^15.2.2", - "@moltin/sdk": "^23.3.0", + "@moltin/sdk": "^25.0.2", "@urql/core": "^3.2.2", "graphql": "^16.6.0", "node-fetch": "2", diff --git a/packages/d2c-schematics/__tests__/application.test.ts b/packages/d2c-schematics/__tests__/application.test.ts index f3f7e091..46a7b4b1 100644 --- a/packages/d2c-schematics/__tests__/application.test.ts +++ b/packages/d2c-schematics/__tests__/application.test.ts @@ -16,7 +16,7 @@ import { Schema as ApplicationOptions } from "../application/schema" describe("Application Schematic", () => { const schematicRunner = new SchematicTestRunner( "@schematics/angular", - require.resolve("../collection.json") + require.resolve("../collection.json"), ) const workspaceOptions: WorkspaceOptions = { @@ -99,31 +99,50 @@ describe("Application Schematic", () => { .toPromise() const files = tree.files - expect(files).toIncludeAllPartialMembers([ - "/src/pages/404.tsx", - "/src/pages/500.tsx", - "/src/pages/_app.tsx", - "/src/pages/configuration-error.tsx", - "/src/lib/resolve-epcc-env.ts", - "/src/lib/parse-cookie.ts", - "/src/components/shimmer.tsx", - "/src/components/toast/toaster.tsx", - "/src/components/layouts/MainLayout.tsx", - "/src/lib/providers/store-provider.ts", - "/src/lib/to-base-64.ts", - "/src/lib/store-wrapper-ssg.ts", - "/src/lib/build-site-navigation.ts", - "/src/lib/epcc-implicit-client.ts", - "/src/lib/epcc-server-client.ts", - "/src/lib/custom-rule-headers.ts", - "/src/lib/ep-client-store.ts", - "/src/lib/resolve-epcc-env.ts", - "/src/lib/is-empty-object.ts", - "/src/lib/menu-style.ts", - "/src/lib/types/read-only-non-empty-array.ts", - "/src/lib/sort-alphabetically.ts", - "/src/lib/get-main-layout.tsx", - "/src/lib/epcc-errors.ts", - ]) + expect(files.filter((f) => f.startsWith("/src/")).sort()).toEqual( + [ + "/src/components/Spinner.tsx", + "/src/components/layouts/MainLayout.tsx", + "/src/components/shared/blurb.tsx", + "/src/components/shimmer.tsx", + "/src/components/toast/toaster.tsx", + "/src/lib/build-site-navigation.ts", + "/src/lib/custom-rule-headers.ts", + "/src/lib/ep-client-store.ts", + "/src/lib/epcc-errors.ts", + "/src/lib/epcc-implicit-client.ts", + "/src/lib/epcc-server-client.ts", + "/src/lib/form-url-encode-body.ts", + "/src/lib/get-main-layout.tsx", + "/src/lib/is-empty-object.ts", + "/src/lib/middleware/cart-cookie-middleware.ts", + "/src/lib/middleware/create-missing-environment-variable-url.ts", + "/src/lib/middleware/implicit-auth-middleware.ts", + "/src/lib/middleware/middleware-runner.ts", + "/src/lib/parse-cookie.ts", + "/src/lib/providers/store-provider.tsx", + "/src/lib/resolve-cart-env.ts", + "/src/lib/resolve-ep-currency-code.ts", + "/src/lib/resolve-epcc-env.ts", + "/src/lib/sort-alphabetically.ts", + "/src/lib/store-wrapper-ssg.ts", + "/src/lib/to-base-64.ts", + "/src/lib/token-expired.ts", + "/src/lib/types/deep-partial.ts", + "/src/lib/types/non-empty-array.ts", + "/src/lib/types/read-only-non-empty-array.ts", + "/src/middleware.ts", + "/src/pages/404.tsx", + "/src/pages/500.tsx", + "/src/pages/_app.tsx", + "/src/pages/about.tsx", + "/src/pages/configuration-error.tsx", + "/src/pages/faq.tsx", + "/src/pages/shipping.tsx", + "/src/pages/terms.tsx", + "/src/services/hierarchy.ts", + "/src/styles/globals.css", + ].sort(), + ) }) }) diff --git a/packages/d2c-schematics/__tests__/header.test.ts b/packages/d2c-schematics/__tests__/header.test.ts index 4de66b34..8db30b63 100644 --- a/packages/d2c-schematics/__tests__/header.test.ts +++ b/packages/d2c-schematics/__tests__/header.test.ts @@ -11,7 +11,7 @@ import { Schema as ApplicationOptions } from "../application/schema" describe("Header Schematic", () => { const schematicRunner = new SchematicTestRunner( "@schematics/angular", - require.resolve("../collection.json") + require.resolve("../collection.json"), ) const workspaceOptions: WorkspaceOptions = { @@ -47,15 +47,19 @@ describe("Header Schematic", () => { .toPromise() const files = tree.files - expect(files).toIncludeAllPartialMembers([ - "/src/components/header/cart/CartMenu.tsx", - "/src/components/header/cart/CartUpdatingSpinner.tsx", - "/src/components/header/cart/ModalCartItem.tsx", - "/src/components/header/navigation/MobileNavBar.tsx", - "/src/components/header/navigation/NavBar.tsx", - "/src/components/header/navigation/NavItemContent.tsx", - "/src/components/header/Header.tsx", - ]) + expect( + files.filter((f) => f.startsWith("/src/components/header/")).sort(), + ).toEqual( + [ + "/src/components/header/cart/CartMenu.tsx", + "/src/components/header/cart/ModalCartItem.tsx", + "/src/components/header/navigation/MobileNavBar.tsx", + "/src/components/header/navigation/NavBar.tsx", + "/src/components/header/navigation/NavItemContent.tsx", + "/src/components/header/Header.tsx", + "/src/components/header/navigation/NavMenu.tsx", + ].sort(), + ) }) it("header schematic should not import search modal module when search=false", async () => { @@ -66,7 +70,7 @@ describe("Header Schematic", () => { const tsSrcFile = createSourceFile( "Header.tsx", tree.readContent("/src/components/header/Header.tsx"), - ScriptTarget.Latest + ScriptTarget.Latest, ) // @ts-ignore @@ -83,7 +87,7 @@ describe("Header Schematic", () => { const tsSrcFile = createSourceFile( "Header.tsx", tree.readContent("/src/components/header/Header.tsx"), - ScriptTarget.Latest + ScriptTarget.Latest, ) // @ts-ignore diff --git a/packages/d2c-schematics/__tests__/product-details-page.test.ts b/packages/d2c-schematics/__tests__/product-details-page.test.ts index 2a9e077d..19e0a3ef 100644 --- a/packages/d2c-schematics/__tests__/product-details-page.test.ts +++ b/packages/d2c-schematics/__tests__/product-details-page.test.ts @@ -9,7 +9,7 @@ import { Schema as ApplicationOptions } from "../application/schema" describe("Product Details Page Schematic", () => { const schematicRunner = new SchematicTestRunner( "@schematics/angular", - require.resolve("../collection.json") + require.resolve("../collection.json"), ) const workspaceOptions: WorkspaceOptions = { @@ -45,27 +45,37 @@ describe("Product Details Page Schematic", () => { .toPromise() const files = tree.files // console.log("files: ", JSON.stringify(files)) - expect(files).toIncludeAllPartialMembers([ - "/src/components/product/ChildProduct.tsx", - "/src/components/product/BaseProduct.tsx", - "/src/components/product/SimpleProduct.tsx", - "/src/components/product/Price.tsx", - "/src/components/product/StrikePrice.tsx", - "/src/components/product/ProductSummary.tsx", - "/src/components/product/ProductExtensions.tsx", - "/src/components/product/ProductDetails.tsx", - "/src/components/product/ProductContainer.tsx", - "/src/components/product/carousel/ProductCarousel.tsx", - "/src/components/product/carousel/ProductCarousel.module.css", - "/src/components/product/carousel/ProductHighlightCarousel.tsx", - "/src/components/product/carousel/CarouselListener.tsx", - "/src/components/product/carousel/HorizontalCarousel.tsx", - "/src/components/product/CartActions.tsx", - "/src/components/product/ProductComponents.tsx", - "/src/components/product/ProductVariations.tsx", - "/src/components/product/variations/ProductVariationColor.tsx", - "/src/components/product/variations/ProductVariationStandard.tsx", - ]) + expect( + files.filter((v) => v.startsWith("/src/components/product/")).sort(), + ).toEqual( + [ + "/src/components/product/CartActions.tsx", + "/src/components/product/Price.tsx", + "/src/components/product/ProductContainer.tsx", + "/src/components/product/ProductDetails.tsx", + "/src/components/product/ProductExtensions.tsx", + "/src/components/product/ProductSummary.tsx", + "/src/components/product/SimpleProduct.tsx", + "/src/components/product/StrikePrice.tsx", + "/src/components/product/bundles/BundleProduct.tsx", + "/src/components/product/bundles/ProductComponent.tsx", + "/src/components/product/bundles/ProductComponents.tsx", + "/src/components/product/bundles/form-parsers.test.ts", + "/src/components/product/bundles/form-parsers.ts", + "/src/components/product/bundles/sort-by-order.ts", + "/src/components/product/bundles/validation-schema.test.ts", + "/src/components/product/bundles/validation-schema.ts", + "/src/components/product/carousel/CarouselListener.tsx", + "/src/components/product/carousel/HorizontalCarousel.tsx", + "/src/components/product/carousel/ProductCarousel.module.css", + "/src/components/product/carousel/ProductCarousel.tsx", + "/src/components/product/carousel/ProductHighlightCarousel.tsx", + "/src/components/product/variations/ProductVariationColor.tsx", + "/src/components/product/variations/ProductVariationStandard.tsx", + "/src/components/product/variations/ProductVariations.tsx", + "/src/components/product/variations/VariationProduct.tsx", + ].sort(), + ) }) it("should create product detail utility files", async () => { @@ -83,7 +93,6 @@ describe("Product Details Page Schematic", () => { "/src/lib/product-helper.ts", "/src/lib/color-lookup.ts", "/src/lib/sort-alphabetically.ts", - "/src/lib/retrieve-product-props.ts", ]) }) diff --git a/packages/d2c-schematics/application/files/public/150-placeholder.png b/packages/d2c-schematics/application/files/public/150-placeholder.png new file mode 100644 index 00000000..61cd0d97 Binary files /dev/null and b/packages/d2c-schematics/application/files/public/150-placeholder.png differ diff --git a/packages/d2c-schematics/application/files/src/lib/menu-style.ts.template b/packages/d2c-schematics/application/files/src/lib/menu-style.ts.template deleted file mode 100644 index 7d0e31ae..00000000 --- a/packages/d2c-schematics/application/files/src/lib/menu-style.ts.template +++ /dev/null @@ -1,11 +0,0 @@ -const menuItemInteractionStyle = { - bg: "none", - color: "brand.primary", -}; - -export const menuItemStyleProps = { - _hover: menuItemInteractionStyle, - _active: menuItemInteractionStyle, - _focus: menuItemInteractionStyle, - color: "gray.500", -}; diff --git a/packages/d2c-schematics/application/files/src/lib/types/deep-partial.ts.template b/packages/d2c-schematics/application/files/src/lib/types/deep-partial.ts.template new file mode 100644 index 00000000..e422dd51 --- /dev/null +++ b/packages/d2c-schematics/application/files/src/lib/types/deep-partial.ts.template @@ -0,0 +1,3 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; diff --git a/packages/d2c-schematics/application/files/src/services/hierarchy.ts.template b/packages/d2c-schematics/application/files/src/services/hierarchy.ts.template index 2418d5db..99ea9279 100644 --- a/packages/d2c-schematics/application/files/src/services/hierarchy.ts.template +++ b/packages/d2c-schematics/application/files/src/services/hierarchy.ts.template @@ -1,7 +1,6 @@ -import type { Node, Hierarchy, ProductResponse } from "@moltin/sdk"; +import type { Node, Hierarchy } from "@moltin/sdk"; import { Moltin as EPCCClient } from "@moltin/sdk"; -import { ShopperCatalogResourcePage } from "@moltin/sdk"; import { getEpccImplicitClient } from "../lib/epcc-implicit-client"; export async function getHierarchies( @@ -37,24 +36,3 @@ export async function getHierarchyNodes( return result.data; } - -export async function getNodeChildren( - nodeId: string, - client?: EPCCClient, -): Promise { - const result = await ( - client ?? getEpccImplicitClient() - ).ShopperCatalog.Nodes.GetNodeChildren({ - nodeId, - }); - return result.data; -} - -export async function getProductsByNode( - nodeId: string, - client?: EPCCClient, -): Promise> { - return await (client ?? getEpccImplicitClient()).ShopperCatalog.Products.With( - ["main_image", "files"], - ).GetProductsByNode({ nodeId }); -} diff --git a/packages/d2c-schematics/cart/files/src/components/cart/Promotion.tsx.template b/packages/d2c-schematics/cart/files/src/components/cart/Promotion.tsx.template index 81389128..19906809 100644 --- a/packages/d2c-schematics/cart/files/src/components/cart/Promotion.tsx.template +++ b/packages/d2c-schematics/cart/files/src/components/cart/Promotion.tsx.template @@ -12,7 +12,7 @@ export const Promotion = (): JSX.Element => { promoCode: "", }; - const { handleSubmit, handleChange, values, errors } = useFormik({ + const { handleSubmit, handleChange, values } = useFormik({ initialValues, onSubmit: async (values) => { await addPromotionToCart(values.promoCode); diff --git a/packages/d2c-schematics/cart/files/src/services/cart.ts.template b/packages/d2c-schematics/cart/files/src/services/cart.ts.template index 49a08498..dd15ad42 100644 --- a/packages/d2c-schematics/cart/files/src/services/cart.ts.template +++ b/packages/d2c-schematics/cart/files/src/services/cart.ts.template @@ -1,52 +1,7 @@ -import type { CartItemsResponse, Moltin as EPCCClient } from "@moltin/sdk"; +import type { Moltin as EPCCClient } from "@moltin/sdk"; import { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk"; import { getEpccImplicitClient } from "../lib/epcc-implicit-client"; -export async function removeCartItem( - id: string, - itemId: string, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()).Cart(id).RemoveItem(itemId); -} - -export async function removeAllCartItems( - id: string, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()).Cart(id).RemoveAllItems(); -} - -export async function updateCartItem( - id: string, - productId: string, - quantity: number, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()) - .Cart(id) - .UpdateItem(productId, quantity); -} - -export async function addPromotion( - id: string, - promoCode: string, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()).Cart(id).AddPromotion(promoCode); -} - -export async function addProductToCart( - cartId: string, - productId: string, - quantity: number, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()) - .Cart(cartId) - .AddProduct(productId, quantity); -} - export interface CustomItemRequest { type: "custom_item"; name: string; @@ -60,16 +15,6 @@ export interface CustomItemRequest { custom_inputs?: Record; } -export async function addCustomItemToCart( - cartId: string, - customItem: CustomItemRequest, - client?: EPCCClient, -): Promise { - return (client ?? getEpccImplicitClient()) - .Cart(cartId) - .AddCustomItem(customItem); -} - export async function getCart( cartId: string, client?: EPCCClient, diff --git a/packages/d2c-schematics/header/files/src/components/header/cart/CartUpdatingSpinner.tsx.template b/packages/d2c-schematics/header/files/src/components/header/cart/CartUpdatingSpinner.tsx.template deleted file mode 100644 index fd1a5b8e..00000000 --- a/packages/d2c-schematics/header/files/src/components/header/cart/CartUpdatingSpinner.tsx.template +++ /dev/null @@ -1,24 +0,0 @@ -export default function CartUpdatingSpinner(): JSX.Element { - return ( - - - - - ); -} diff --git a/packages/d2c-schematics/package.json b/packages/d2c-schematics/package.json index 5d517462..b89b2a58 100644 --- a/packages/d2c-schematics/package.json +++ b/packages/d2c-schematics/package.json @@ -23,7 +23,7 @@ "@angular-devkit/schematics": "^14.1.0", "@elasticpath/composable-common": "*", "@elasticpath/composable-integration-hub-deployer": "*", - "@moltin/sdk": "^23.3.0", + "@moltin/sdk": "^25.0.2", "@trpc/client": "^10.14.1", "algoliasearch": "^4.15.0", "ansi-colors": "4.1.3", diff --git a/packages/d2c-schematics/product-details-page/files/src/components/product/BaseProduct.tsx.template b/packages/d2c-schematics/product-details-page/files/src/components/product/BaseProduct.tsx.template deleted file mode 100644 index 6d2fda90..00000000 --- a/packages/d2c-schematics/product-details-page/files/src/components/product/BaseProduct.tsx.template +++ /dev/null @@ -1,31 +0,0 @@ -import { IBaseProduct } from "../../lib/types/product-types"; -import ProductVariations from "./ProductVariations"; -import ProductContainer from "./ProductContainer"; - -interface IBaseProductDetail { - baseProduct: IBaseProduct; -} - -const BaseProductDetail = ({ - baseProduct, -}: IBaseProductDetail): JSX.Element => { - const { - product: { attributes, id }, - variations, - variationsMatrix, - } = baseProduct; - return ( - - {variations && ( - - )} - - ); -}; - -export default BaseProductDetail; diff --git a/packages/d2c-schematics/product-details-page/files/src/components/product/CartActions.tsx.template b/packages/d2c-schematics/product-details-page/files/src/components/product/CartActions.tsx.template index c303c75a..5429bfc2 100644 --- a/packages/d2c-schematics/product-details-page/files/src/components/product/CartActions.tsx.template +++ b/packages/d2c-schematics/product-details-page/files/src/components/product/CartActions.tsx.template @@ -4,13 +4,9 @@ import { useCart } from "@elasticpath/react-shopper-hooks"; import Spinner from "../Spinner"; import clsx from "clsx"; -interface ICartActions { - productId: string; -} - -const CartActions = ({ productId }: ICartActions): JSX.Element => { +const CartActions = (): JSX.Element => { const context = useContext(ProductContext); - const { addProductToCart, isUpdatingCart } = useCart(); + const { isUpdatingCart } = useCart(); return (
{ >
))} diff --git a/packages/d2c-schematics/product-details-page/files/src/components/product/variations/ProductVariationStandard.tsx.template b/packages/d2c-schematics/product-details-page/files/src/components/product/variations/ProductVariationStandard.tsx.template index a9315791..a326191c 100644 --- a/packages/d2c-schematics/product-details-page/files/src/components/product/variations/ProductVariationStandard.tsx.template +++ b/packages/d2c-schematics/product-details-page/files/src/components/product/variations/ProductVariationStandard.tsx.template @@ -1,4 +1,5 @@ import clsx from "clsx"; +import type { useVariationProduct } from "@elasticpath/react-shopper-hooks"; interface ProductVariationOption { id: string; @@ -16,7 +17,9 @@ interface IProductVariation { name: string; options: ProductVariationOption[]; }; - updateOptionHandler: UpdateOptionHandler; + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"]; selectedOptionId?: string; } @@ -31,6 +34,7 @@ const ProductVariationStandard = ({
{variation.options.map((o) => ( diff --git a/packages/d2c-schematics/product-details-page/files/src/components/product/ProductVariations.tsx.template b/packages/d2c-schematics/product-details-page/files/src/components/product/variations/ProductVariations.tsx.template similarity index 53% rename from packages/d2c-schematics/product-details-page/files/src/components/product/ProductVariations.tsx.template rename to packages/d2c-schematics/product-details-page/files/src/components/product/variations/ProductVariations.tsx.template index 7332a23c..f1c4f524 100644 --- a/packages/d2c-schematics/product-details-page/files/src/components/product/ProductVariations.tsx.template +++ b/packages/d2c-schematics/product-details-page/files/src/components/product/variations/ProductVariations.tsx.template @@ -1,28 +1,16 @@ import type { CatalogsProductVariation } from "@moltin/sdk"; import { useRouter } from "next/router"; import { useContext } from "react"; -import { useEffect, useState } from "react"; -import { OptionDict } from "../../lib/types/product-types"; -import { createEmptyOptionDict, ProductContext } from "../../lib/product-util"; +import { useEffect } from "react"; +import { OptionDict } from "../../../lib/types/product-types"; +import { ProductContext } from "../../../lib/product-util"; import { allVariationsHaveSelectedOption, - getOptionsFromSkuId, getSkuIdFromOptions, - mapOptionsToVariation, -} from "../../lib/product-helper"; -import ProductVariationStandard, { - UpdateOptionHandler, -} from "./variations/ProductVariationStandard"; -import ProductVariationColor from "./variations/ProductVariationColor"; -import { MatrixObjectEntry } from "../../lib/types/matrix-object-entry"; - -interface IProductVariations { - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; - baseProductSlug: string; - currentSkuId: string; - skuOptions?: string[]; -} +} from "../../../lib/product-helper"; +import ProductVariationStandard from "./ProductVariationStandard"; +import ProductVariationColor from "./ProductVariationColor"; +import { useVariationProduct } from "@elasticpath/react-shopper-hooks"; const getSelectedOption = ( variationId: string, @@ -31,20 +19,19 @@ const getSelectedOption = ( return optionLookupObj[variationId]; }; -const ProductVariations = ({ - variations, - baseProductSlug, - currentSkuId, - variationsMatrix, -}: IProductVariations): JSX.Element => { - const currentSkuOptions = getOptionsFromSkuId(currentSkuId, variationsMatrix); - const initialOptions = currentSkuOptions - ? mapOptionsToVariation(currentSkuOptions, variations) - : createEmptyOptionDict(variations); +const ProductVariations = (): JSX.Element => { + const { + variations, + variationsMatrix, + product, + selectedOptions, + updateSelectedOptions, + } = useVariationProduct(); + + const currentProductId = product.response.id; const context = useContext(ProductContext); - const [selectedOptions, setSelectedOptions] = - useState(initialOptions); + const router = useRouter(); useEffect(() => { @@ -56,7 +43,7 @@ const ProductVariations = ({ if ( !context?.isChangingSku && selectedSkuId && - selectedSkuId !== currentSkuId && + selectedSkuId !== currentProductId && allVariationsHaveSelectedOption(selectedOptions, variations) ) { context?.setIsChangingSku(true); @@ -66,28 +53,13 @@ const ProductVariations = ({ } }, [ selectedOptions, - baseProductSlug, context, - currentSkuId, + currentProductId, router, variations, variationsMatrix, ]); - const updateOptionHandler: UpdateOptionHandler = - (variationId) => - (optionId): void => { - for (const selectedOptionKey in selectedOptions) { - if (selectedOptionKey === variationId) { - setSelectedOptions({ - ...selectedOptions, - [selectedOptionKey]: optionId, - }); - break; - } - } - }; - return (
resolveVariationComponentByName( v, - updateOptionHandler, + updateSelectedOptions, getSelectedOption(v.id, selectedOptions), ), )} @@ -107,7 +79,9 @@ const ProductVariations = ({ function resolveVariationComponentByName( v: CatalogsProductVariation, - updateOptionHandler: UpdateOptionHandler, + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"], selectedOptionId?: string, ): JSX.Element { switch (v.name.toLowerCase()) { diff --git a/packages/d2c-schematics/product-details-page/files/src/components/product/variations/VariationProduct.tsx.template b/packages/d2c-schematics/product-details-page/files/src/components/product/variations/VariationProduct.tsx.template new file mode 100644 index 00000000..23d30a85 --- /dev/null +++ b/packages/d2c-schematics/product-details-page/files/src/components/product/variations/VariationProduct.tsx.template @@ -0,0 +1,43 @@ +import { + useCart, + useVariationProduct, + VariationProduct, + VariationProductProvider, +} from "@elasticpath/react-shopper-hooks"; +import { useCallback } from "react"; +import { Formik } from "formik"; +import ProductContainer from "../ProductContainer"; +import ProductVariations from "./ProductVariations"; + +export const VariationProductDetail = ({ + variationProduct, +}: { + variationProduct: VariationProduct; +}): JSX.Element => { + return ( + + + + ); +}; + +export function VariationProductContainer(): JSX.Element { + const { addProductToCart } = useCart(); + const { product } = useVariationProduct(); + + const { + response: { id }, + } = product; + + const submit = useCallback(async () => { + await addProductToCart(id, 1); + }, [id, addProductToCart]); + + return ( + submit()}> + + + + + ); +} diff --git a/packages/d2c-schematics/product-details-page/files/src/lib/product-util.test.ts.template b/packages/d2c-schematics/product-details-page/files/src/lib/product-util.test.ts.template index 0e301a03..d1882706 100644 --- a/packages/d2c-schematics/product-details-page/files/src/lib/product-util.test.ts.template +++ b/packages/d2c-schematics/product-details-page/files/src/lib/product-util.test.ts.template @@ -10,7 +10,6 @@ import { excludeChildProducts, filterBaseProducts, getProductMainImage, - getProductOtherImageUrls, processImageFiles, } from "./product-util"; @@ -134,47 +133,6 @@ describe("product util", () => { expect(processImageFiles(files as File[])).toEqual(expected); }); - test("getProductOtherImageUrls should return a products images files not including the main image", () => { - const files: Partial[] = [ - { - type: "file", - id: "123", - mime_type: "image/jpeg", - }, - { - type: "file", - id: "456", - mime_type: "image/jpeg", - }, - ]; - - const mainImageFile: Partial = { - type: "file", - id: "123", - mime_type: "image/jpeg", - }; - - const productResp: Partial> = { - included: { - files: files as File[], - main_images: [mainImageFile] as File[], - }, - }; - - const expected: Partial[] = [ - { - type: "file", - id: "456", - mime_type: "image/jpeg", - }, - ]; - expect( - getProductOtherImageUrls( - productResp as ShopperCatalogResource, - ), - ).toEqual(expected); - }); - test("getProductMainImage should return a products main image file", () => { const mainImageFile: Partial = { type: "file", @@ -188,11 +146,9 @@ describe("product util", () => { }, }; - expect( - getProductMainImage( - productResp as ShopperCatalogResource, - ), - ).toEqual(mainImageFile); + expect(getProductMainImage(productResp.included?.main_images)).toEqual( + mainImageFile, + ); }); test("getProductMainImage should return null when product does not have main image included", () => { @@ -200,11 +156,9 @@ describe("product util", () => { included: {}, }; - expect( - getProductMainImage( - productResp as ShopperCatalogResource, - ), - ).toEqual(null); + expect(getProductMainImage(productResp.included?.main_images)).toEqual( + null, + ); }); test("createEmptyOptionDict should return an OptionDict with all with variation keys assigned undefined values", () => { diff --git a/packages/d2c-schematics/product-details-page/files/src/lib/product-util.ts.template b/packages/d2c-schematics/product-details-page/files/src/lib/product-util.ts.template index 63f78260..fa9a0eb6 100644 --- a/packages/d2c-schematics/product-details-page/files/src/lib/product-util.ts.template +++ b/packages/d2c-schematics/product-details-page/files/src/lib/product-util.ts.template @@ -2,7 +2,6 @@ import type { File, ProductResponse, CatalogsProductVariation, - ShopperCatalogResource, } from "@moltin/sdk"; import { createContext } from "react"; import type { @@ -30,19 +29,10 @@ export function processImageFiles(files: File[], mainImageId?: string) { ); } -export function getProductOtherImageUrls( - productResp: ShopperCatalogResource, -): File[] { - const files = productResp?.included?.files; - return files - ? processImageFiles(files, productResp?.included?.main_images?.[0].id) - : []; -} - export function getProductMainImage( - productResp: ShopperCatalogResource, + mainImages: File[] | undefined, ): File | null { - return productResp?.included?.main_images?.[0] || null; + return mainImages?.[0] || null; } // Using existance of parent relationship property to filter because only child products seem to have this property. diff --git a/packages/d2c-schematics/product-details-page/files/src/lib/retrieve-product-props.ts.template b/packages/d2c-schematics/product-details-page/files/src/lib/retrieve-product-props.ts.template deleted file mode 100644 index 7ed550f8..00000000 --- a/packages/d2c-schematics/product-details-page/files/src/lib/retrieve-product-props.ts.template +++ /dev/null @@ -1,88 +0,0 @@ -import { ProductResponse, Resource, ShopperCatalogResource } from "@moltin/sdk"; -import { GetStaticPropsResult } from "next"; -import { - IBaseProduct, - IChildProduct, - ISimpleProduct, -} from "./types/product-types"; -import { getProductMainImage, getProductOtherImageUrls } from "./product-util"; -import { getProductById } from "../services/products"; -import { sortAlphabetically } from "./sort-alphabetically"; - -export function retrieveSimpleProps( - productResource: ShopperCatalogResource, -): GetStaticPropsResult { - const component_products = productResource.included?.component_products; - return { - props: { - kind: "simple-product", - product: productResource.data, - main_image: getProductMainImage(productResource), - otherImages: getProductOtherImageUrls(productResource), - ...(component_products && { component_products }), - }, - }; -} - -export async function retrieveChildProps( - childProductResource: Resource, -): Promise> { - const baseProductId = childProductResource.data.attributes.base_product_id; - const baseProduct = await getProductById(baseProductId); - if (!baseProduct) { - throw Error( - `Unable to retrieve child props, failed to get the base product for ${baseProductId}`, - ); - } - const { - data: { - meta: { variation_matrix, variations }, - }, - } = baseProduct; - - if (!variations || !variation_matrix) { - throw Error( - `Unable to retrieve child props, failed to get the variations or variation_matrix from base product for ${baseProductId}`, - ); - } - - return { - props: { - kind: "child-product", - product: childProductResource.data, - baseProduct: baseProduct.data, - main_image: getProductMainImage(childProductResource), - otherImages: getProductOtherImageUrls(childProductResource), - variationsMatrix: variation_matrix, - variations: variations.sort(sortAlphabetically), - }, - }; -} - -export async function retrieveBaseProps( - baseProductResource: Resource, -): Promise> { - const { - data: { - meta: { variations, variation_matrix }, - attributes: { slug }, - }, - } = baseProductResource; - - if (!variations || !variation_matrix) { - throw Error( - `Unable to retrieve base product props, failed to get the variations or variation_matrix from base product for ${slug}`, - ); - } - - return { - props: { - kind: "base-product", - product: baseProductResource.data, - main_image: getProductMainImage(baseProductResource), - otherImages: getProductOtherImageUrls(baseProductResource), - variationsMatrix: variation_matrix, - variations: variations.sort(sortAlphabetically), - }, - }; -} diff --git a/packages/d2c-schematics/product-details-page/files/src/lib/types/product-types.ts.template b/packages/d2c-schematics/product-details-page/files/src/lib/types/product-types.ts.template index ecbfa38a..a18a260d 100644 --- a/packages/d2c-schematics/product-details-page/files/src/lib/types/product-types.ts.template +++ b/packages/d2c-schematics/product-details-page/files/src/lib/types/product-types.ts.template @@ -1,42 +1,11 @@ -import type { - ProductResponse, - CatalogsProductVariation, - File, -} from "@moltin/sdk"; +import type { ProductResponse, File } from "@moltin/sdk"; import type { Dispatch, SetStateAction } from "react"; -import { MatrixObjectEntry } from "./matrix-object-entry"; export type IdentifiableBaseProduct = ProductResponse & { id: string; attributes: { slug: string; sku: string; base_product: true }; }; -export interface IBase { - product: ProductResponse; - main_image: File | null; - otherImages: File[]; - component_products?: ProductResponse[]; -} - -export interface IBaseProduct extends IBase { - kind: "base-product"; - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; -} - -export interface IChildProduct extends IBase { - kind: "child-product"; - baseProduct: ProductResponse; - variations: CatalogsProductVariation[]; - variationsMatrix: MatrixObjectEntry; -} - -export interface ISimpleProduct extends IBase { - kind: "simple-product"; -} - -export type IProduct = IBaseProduct | IChildProduct | ISimpleProduct; - export interface ProductContextState { isChangingSku: boolean; setIsChangingSku: Dispatch>; diff --git a/packages/d2c-schematics/product-details-page/files/src/pages/products/[productId].tsx.template b/packages/d2c-schematics/product-details-page/files/src/pages/products/[productId].tsx.template index 073bdf8e..07f50972 100644 --- a/packages/d2c-schematics/product-details-page/files/src/pages/products/[productId].tsx.template +++ b/packages/d2c-schematics/product-details-page/files/src/pages/products/[productId].tsx.template @@ -1,30 +1,28 @@ import type { GetStaticPaths } from "next"; import { ParsedUrlQuery } from "querystring"; -import ChildProductDetail from "../../components/product/ChildProduct"; -import BaseProductDetail from "../../components/product/BaseProduct"; -import { - isChildProductResource, - isSimpleProductResource, -} from "../../lib/product-helper"; + import { getProductById } from "../../services/products"; import SimpleProductDetail from "../../components/product/SimpleProduct"; import { ProductContext } from "../../lib/product-util"; import React, { ReactElement, useState } from "react"; -import type { IProduct } from "../../lib/types/product-types"; +import type { ShopperProduct } from "@elasticpath/react-shopper-hooks"; import { withStoreStaticProps } from "../../lib/store-wrapper-ssg"; -import { - retrieveBaseProps, - retrieveChildProps, - retrieveSimpleProps, -} from "../../lib/retrieve-product-props"; import Head from "next/head"; import { NextPageWithLayout } from "../_app"; import MainLayout, { MAIN_LAYOUT_TITLE, } from "../../components/layouts/MainLayout"; +import BundleProductDetail from "../../components/product/bundles/BundleProduct"; +import { parseProductResponse } from "@elasticpath/react-shopper-hooks"; +import { getEpccImplicitClient } from "../../lib/epcc-implicit-client"; +import { VariationProductDetail } from "../../components/product/variations/VariationProduct"; + +interface ProductPageProps { + product: ShopperProduct; +} -export const Product: NextPageWithLayout = (props: IProduct) => { +export const Product: NextPageWithLayout = (props) => { const [isChangingSku, setIsChangingSku] = useState(false); const { product } = props; @@ -32,7 +30,7 @@ export const Product: NextPageWithLayout = (props: IProduct) => { return (
= (props: IProduct) => { setIsChangingSku, }} > - {resolveProductDetailComponent(props)} + {resolveProductDetailComponent(product)}
); }; Product.getLayout = function getLayout(page: ReactElement, pageProps, ctx?) { + const { + attributes: { name, description }, + } = pageProps.product.response; + return ( <> {page} - - {`${MAIN_LAYOUT_TITLE} - ${pageProps.product.attributes.name}`} - - - - + {`${MAIN_LAYOUT_TITLE} - ${name}`} + + + ); }; -function resolveProductDetailComponent(props: IProduct): JSX.Element { - switch (props.kind) { +function resolveProductDetailComponent(product: ShopperProduct): JSX.Element { + switch (product.kind) { case "base-product": - return ; + return ; case "child-product": - return ; + return ; case "simple-product": - return ; + return ; + case "bundle-product": + return ; } } @@ -87,7 +80,7 @@ interface ProductRouteParams extends ParsedUrlQuery { } export const getStaticProps = withStoreStaticProps< - IProduct, + ProductPageProps, ProductRouteParams >(async ({ params }) => { if (!params) { @@ -104,16 +97,15 @@ export const getStaticProps = withStoreStaticProps< }; } - const productData = product.data; - - const retrievedResults = isSimpleProductResource(productData) - ? retrieveSimpleProps(product) - : isChildProductResource(productData) - ? await retrieveChildProps(product) - : await retrieveBaseProps(product); + const shopperProduct = await parseProductResponse( + product, + getEpccImplicitClient(), + ); return { - ...retrievedResults, + props: { + product: shopperProduct, + }, revalidate: 60, }; }); diff --git a/packages/d2c-schematics/product-list-page-algolia/files/src/components/search/Hit.tsx.template b/packages/d2c-schematics/product-list-page-algolia/files/src/components/search/Hit.tsx.template index a439e140..e83afd2b 100644 --- a/packages/d2c-schematics/product-list-page-algolia/files/src/components/search/Hit.tsx.template +++ b/packages/d2c-schematics/product-list-page-algolia/files/src/components/search/Hit.tsx.template @@ -8,18 +8,12 @@ import { Dialog, Transition } from "@headlessui/react"; import { Fragment, useEffect, useState } from "react"; import Product from "../product-modal/Product"; import { getProductById } from "../../services/products"; -import { - isChildProductResource, - isSimpleProductResource, -} from "../../lib/product-helper"; -import { - retrieveBaseProps, - retrieveChildProps, - retrieveSimpleProps, -} from "../../lib/retrieve-product-props"; -import { IProduct } from "../../lib/types/product-types"; -import { GetStaticPropsResult } from "next/types"; import { EyeSlashIcon, XMarkIcon } from "@heroicons/react/24/solid"; +import { + parseProductResponse, + ShopperProduct, +} from "@elasticpath/react-shopper-hooks"; +import { getEpccImplicitClient } from "../../lib/epcc-implicit-client"; export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element { const { ep_price, ep_name, objectID, ep_main_image_url, ep_description } = @@ -38,17 +32,14 @@ export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element { const fetchProduct = async (id: string) => { const product = await getProductById(id); - const productData = product.data; - const retrievedResults = isSimpleProductResource(productData) - ? retrieveSimpleProps(product) - : isChildProductResource(productData) - ? await retrieveChildProps(product) - : await retrieveBaseProps(product); - setProductProps(retrievedResults); + const retrievedResults = await parseProductResponse( + product, + getEpccImplicitClient(), + ); + setProduct(retrievedResults); }; - const [productProps, setProductProps] = - useState>(); + const [product, setProduct] = useState(); useEffect(() => { isOpen && fetchProduct(objectID); @@ -112,7 +103,8 @@ export default function HitComponent({ hit }: { hit: SearchHit }): JSX.Element {