diff --git a/assets/js/atomic/blocks/product-elements/button/block.js b/assets/js/atomic/blocks/product-elements/button/block.js index 4283ae68c7d..579424b2f1b 100644 --- a/assets/js/atomic/blocks/product-elements/button/block.js +++ b/assets/js/atomic/blocks/product-elements/button/block.js @@ -35,7 +35,7 @@ import './style.scss'; * @param {string} [props.className] CSS Class name for the component. * @return {*} The component. */ -const Block = ( props ) => { +export const Block = ( props ) => { const { className } = props; const { parentClassName } = useInnerBlockLayoutContext(); diff --git a/assets/js/atomic/blocks/product-elements/image/block.js b/assets/js/atomic/blocks/product-elements/image/block.js index e55f875c133..bc3e1cafab5 100644 --- a/assets/js/atomic/blocks/product-elements/image/block.js +++ b/assets/js/atomic/blocks/product-elements/image/block.js @@ -27,13 +27,13 @@ import './style.scss'; /** * Product Image Block Component. * - * @param {Object} props Incoming props. - * @param {string} [props.className] CSS Class name for the component. - * @param {string} [props.imageSizing] Size of image to use. - * @param {boolean} [props.showProductLink] Whether or not to display a link to the product page. - * @param {boolean} [props.showSaleBadge] Whether or not to display the on sale badge. - * @param {string} [props.saleBadgeAlign] How should the sale badge be aligned if displayed. - * @param {boolean} [props.isDescendentOfQueryLoop] Whether or not be a children of Query Loop Block. + * @param {Object} props Incoming props. + * @param {string} [props.className] CSS Class name for the component. + * @param {string|undefined} [props.imageSizing] Size of image to use. + * @param {boolean|undefined} [props.showProductLink] Whether or not to display a link to the product page. + * @param {boolean} [props.showSaleBadge] Whether or not to display the on sale badge. + * @param {string|undefined} [props.saleBadgeAlign] How should the sale badge be aligned if displayed. + * @param {boolean} [props.isDescendentOfQueryLoop] Whether or not be a children of Query Loop Block. * @return {*} The component. */ export const Block = ( props ) => { diff --git a/assets/js/atomic/blocks/product-elements/price/block.js b/assets/js/atomic/blocks/product-elements/price/block.js index b57fc475dc9..d6872ccf635 100644 --- a/assets/js/atomic/blocks/product-elements/price/block.js +++ b/assets/js/atomic/blocks/product-elements/price/block.js @@ -25,7 +25,7 @@ import { withProductDataContext } from '@woocommerce/shared-hocs'; * context will be used if this is not provided. * @return {*} The component. */ -const Block = ( props ) => { +export const Block = ( props ) => { const { className, textAlign } = props; const { parentClassName } = useInnerBlockLayoutContext(); const { product } = useProductDataContext(); diff --git a/assets/js/atomic/blocks/product-elements/rating/block.js b/assets/js/atomic/blocks/product-elements/rating/block.js index a42c78f1a02..c1dacb14abd 100644 --- a/assets/js/atomic/blocks/product-elements/rating/block.js +++ b/assets/js/atomic/blocks/product-elements/rating/block.js @@ -27,7 +27,7 @@ import './style.scss'; * @param {string} [props.className] CSS Class name for the component. * @return {*} The component. */ -const Block = ( props ) => { +export const Block = ( props ) => { const { parentClassName } = useInnerBlockLayoutContext(); const { product } = useProductDataContext(); const rating = getAverageRating( product ); diff --git a/assets/js/atomic/blocks/product-elements/sale-badge/block.js b/assets/js/atomic/blocks/product-elements/sale-badge/block.js index d44cab0a773..bfd05e7171b 100644 --- a/assets/js/atomic/blocks/product-elements/sale-badge/block.js +++ b/assets/js/atomic/blocks/product-elements/sale-badge/block.js @@ -30,7 +30,7 @@ import './style.scss'; * @param {string} [props.align] Alignment of the badge. * @return {*} The component. */ -const Block = ( props ) => { +export const Block = ( props ) => { const { className, align } = props; const { parentClassName } = useInnerBlockLayoutContext(); const { product } = useProductDataContext(); diff --git a/assets/js/atomic/blocks/product-elements/title/types.ts b/assets/js/atomic/blocks/product-elements/title/types.ts index 890ba7fecc5..68e77d0db39 100644 --- a/assets/js/atomic/blocks/product-elements/title/types.ts +++ b/assets/js/atomic/blocks/product-elements/title/types.ts @@ -2,7 +2,6 @@ export interface Attributes { headingLevel: number; showProductLink: boolean; linkTarget?: string; - productId: number; align: string; } diff --git a/assets/js/base/context/hooks/cart/test/use-store-cart.js b/assets/js/base/context/hooks/cart/test/use-store-cart.js index ffd5d7e1666..c92712b8d10 100644 --- a/assets/js/base/context/hooks/cart/test/use-store-cart.js +++ b/assets/js/base/context/hooks/cart/test/use-store-cart.js @@ -30,6 +30,7 @@ describe( 'useStoreCart', () => { const previewCartData = { cartCoupons: previewCart.coupons, cartItems: previewCart.items, + crossSellsProducts: previewCart.cross_sells, cartFees: previewCart.fees, cartItemsCount: previewCart.items_count, cartItemsWeight: previewCart.items_weight, diff --git a/assets/js/base/context/hooks/cart/use-store-cart.ts b/assets/js/base/context/hooks/cart/use-store-cart.ts index ff130605775..611d40371b8 100644 --- a/assets/js/base/context/hooks/cart/use-store-cart.ts +++ b/assets/js/base/context/hooks/cart/use-store-cart.ts @@ -9,6 +9,7 @@ import { CART_STORE_KEY as storeKey, EMPTY_CART_COUPONS, EMPTY_CART_ITEMS, + EMPTY_CART_CROSS_SELLS, EMPTY_CART_FEES, EMPTY_CART_ITEM_ERRORS, EMPTY_CART_ERRORS, @@ -99,6 +100,7 @@ export const defaultCartData: StoreCart = { cartFees: EMPTY_CART_FEES, cartItemsCount: 0, cartItemsWeight: 0, + crossSellsProducts: EMPTY_CART_CROSS_SELLS, cartNeedsPayment: true, cartNeedsShipping: true, cartItemErrors: EMPTY_CART_ITEM_ERRORS, @@ -150,6 +152,7 @@ export const useStoreCart = ( return { cartCoupons: previewCart.coupons, cartItems: previewCart.items, + crossSellsProducts: previewCart.cross_sells, cartFees: previewCart.fees, cartItemsCount: previewCart.items_count, cartItemsWeight: previewCart.items_weight, @@ -211,6 +214,7 @@ export const useStoreCart = ( return { cartCoupons, cartItems: cartData.items, + crossSellsProducts: cartData.crossSells, cartFees, cartItemsCount: cartData.itemsCount, cartItemsWeight: cartData.itemsWeight, diff --git a/assets/js/blocks/cart/cart-cross-sells-product-list/cart-cross-sells-product.tsx b/assets/js/blocks/cart/cart-cross-sells-product-list/cart-cross-sells-product.tsx new file mode 100644 index 00000000000..067844d2116 --- /dev/null +++ b/assets/js/blocks/cart/cart-cross-sells-product-list/cart-cross-sells-product.tsx @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { + InnerBlockLayoutContextProvider, + ProductDataContextProvider, +} from '@woocommerce/shared-context'; +import { ProductResponseItem } from '@woocommerce/type-defs/product-response'; + +/** + * Internal dependencies + */ +import { Block as ProductImage } from '../../../atomic/blocks/product-elements/image/block'; +import { Block as ProductName } from '../../../atomic/blocks/product-elements/title/block'; +import { Block as ProductRating } from '../../../atomic/blocks/product-elements/rating/block'; +import { Block as ProductSaleBadge } from '../../../atomic/blocks/product-elements/sale-badge/block'; +import { Block as ProductPrice } from '../../../atomic/blocks/product-elements/price/block'; +import { Block as ProductButton } from '../../../atomic/blocks/product-elements/button/block'; +import AddToCartButton from '../../../atomic/blocks/product-elements/add-to-cart/block'; + +interface CrossSellsProductProps { + product: ProductResponseItem; + isLoading: boolean; +} + +const CartCrossSellsProduct = ( { + product, +}: CrossSellsProductProps ): JSX.Element => { + return ( +
+ + +
+ + + + + +
+ { product.is_in_stock ? ( + + ) : ( + + ) } +
+
+
+ ); +}; + +export default CartCrossSellsProduct; diff --git a/assets/js/blocks/cart/cart-cross-sells-product-list/index.tsx b/assets/js/blocks/cart/cart-cross-sells-product-list/index.tsx new file mode 100644 index 00000000000..142d91030ce --- /dev/null +++ b/assets/js/blocks/cart/cart-cross-sells-product-list/index.tsx @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import { ProductResponseItem } from '@woocommerce/type-defs/product-response'; + +/** + * Internal dependencies + */ +import CartCrossSellsProduct from './cart-cross-sells-product'; + +interface CrossSellsProductListProps { + products: ProductResponseItem[]; + className?: string | undefined; + columns: number; +} + +const CartCrossSellsProductList = ( { + products, + columns, +}: CrossSellsProductListProps ): JSX.Element => { + const crossSellsProducts = products.map( ( product, i ) => { + if ( i >= columns ) return null; + + return ( + + ); + } ); + + return
{ crossSellsProducts }
; +}; + +export default CartCrossSellsProductList; diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/block.json b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/block.json new file mode 100644 index 00000000000..5c8c562ff1f --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/block.json @@ -0,0 +1,25 @@ +{ + "name": "woocommerce/cart-cross-sells-block", + "version": "1.0.0", + "title": "Cart Cross-Sells block", + "description": "Shows the Cross-Sells block.", + "category": "woocommerce", + "supports": { + "align": false, + "html": false, + "multiple": false, + "reusable": false, + "inserter": true + }, + "attributes": { + "lock": { + "type": "object", + "default": { + "move": true + } + } + }, + "parent": [ "woocommerce/cart-items-block" ], + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2 +} diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx new file mode 100644 index 00000000000..25d8a8a9b20 --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import type { TemplateArray } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; + +export const Edit = (): JSX.Element => { + const blockProps = useBlockProps( { + className: 'wc-block-cart__cross-sells', + } ); + const defaultTemplate = [ + [ + 'core/heading', + { + content: __( + 'You may be interested in…', + 'woo-gutenberg-products-block' + ), + level: 3, + }, + , + [], + ], + [ 'woocommerce/cart-cross-sells-products-block', {}, [] ], + ] as TemplateArray; + + return ( +
+ +
+ ); +}; + +export const Save = (): JSX.Element => { + return ( +
+ +
+ ); +}; diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/frontend.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/frontend.tsx new file mode 100644 index 00000000000..bdf825767a2 --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/frontend.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { useStoreCart } from '@woocommerce/base-context/hooks'; + +interface Props { + children?: JSX.Element | JSX.Element[]; + className?: string; +} + +const FrontendBlock = ( { + children, + className = '', +}: Props ): JSX.Element | null => { + const { crossSellsProducts, cartIsLoading } = useStoreCart(); + + if ( cartIsLoading || crossSellsProducts.length < 1 ) { + return null; + } + + return
{ children }
; +}; + +export default FrontendBlock; diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/index.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/index.tsx new file mode 100644 index 00000000000..857f713e16f --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/index.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { Icon, column } from '@wordpress/icons'; +import { registerExperimentalBlockType } from '@woocommerce/block-settings'; + +/** + * Internal dependencies + */ +import { Edit, Save } from './edit'; +import metadata from './block.json'; + +registerExperimentalBlockType( metadata, { + icon: { + src: ( + + ), + }, + edit: Edit, + save: Save, +} ); diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.json b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.json new file mode 100644 index 00000000000..c867f2ca2ad --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.json @@ -0,0 +1,31 @@ +{ + "name": "woocommerce/cart-cross-sells-products-block", + "version": "1.0.0", + "title": "Cart Cross-Sells products", + "description": "Shows the Cross-Sells products.", + "category": "woocommerce", + "supports": { + "align": false, + "html": false, + "multiple": false, + "reusable": false, + "inserter": false, + "lock": false + }, + "attributes": { + "columns": { + "type": "number", + "default": 3 + }, + "lock": { + "type": "object", + "default": { + "remove": true, + "move": true + } + } + }, + "parent": [ "woocommerce/cart-cross-sells-block" ], + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2 +} diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.tsx new file mode 100644 index 00000000000..645f8499b25 --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.tsx @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { useStoreCart } from '@woocommerce/base-context/hooks'; + +/** + * Internal dependencies + */ +import CartCrossSellsProductList from '../../cart-cross-sells-product-list'; +import metadata from './block.json'; + +interface BlockProps { + className?: string | undefined; + columns: number; +} + +const Block = ( { className, columns }: BlockProps ): JSX.Element => { + const { crossSellsProducts } = useStoreCart(); + + if ( typeof columns === 'undefined' ) { + columns = metadata.attributes.columns.default; + } + + return ( + + ); +}; + +export default Block; diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/edit.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/edit.tsx new file mode 100644 index 00000000000..3c0dedcb7a2 --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/edit.tsx @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PanelBody, RangeControl } from '@wordpress/components'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { getSetting } from '@woocommerce/settings'; +import Noninteractive from '@woocommerce/base-components/noninteractive'; + +/** + * Internal dependencies + */ +import Block from './block'; +import './editor.scss'; + +interface Attributes { + className?: string; + columns: number; +} + +interface Props { + attributes: Attributes; + setAttributes: ( attributes: Record< string, unknown > ) => void; +} + +export const Edit = ( { attributes, setAttributes }: Props ): JSX.Element => { + const { className, columns } = attributes; + const blockProps = useBlockProps(); + + return ( +
+ + + + setAttributes( { columns: value } ) + } + min={ getSetting( 'min_columns', 1 ) } + max={ getSetting( 'max_columns', 6 ) } + /> + + + + + +
+ ); +}; + +export const Save = (): JSX.Element => { + return
; +}; diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/editor.scss b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/editor.scss new file mode 100644 index 00000000000..77083516b47 --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/editor.scss @@ -0,0 +1,38 @@ +.wp-block-woocommerce-cart-cross-sells-products-block { + + .cross-sells-product { + display: inline-block; + margin-bottom: 2em; + padding-right: 5%; + text-align: center; + vertical-align: top; + width: 30%; + + &:nth-child(3n + 3) { + padding-right: 0; + } + + div { + .wc-block-components-product-name { + font-weight: 400; + } + + .wc-block-components-product-price { + display: block; + } + } + + .wc-block-components-product-add-to-cart-button:not(.is-link) { + background-color: #eee; + color: #333; + margin-top: 1em; + + &:focus, + &:hover { + background-color: #d5d5d5; + border-color: #d5d5d5; + color: #333; + } + } + } +} diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/frontend.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/frontend.tsx new file mode 100644 index 00000000000..4fc9ad2897a --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/frontend.tsx @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import Block from './block'; + +export default Block; diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/index.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/index.tsx new file mode 100644 index 00000000000..857f713e16f --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/index.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { Icon, column } from '@wordpress/icons'; +import { registerExperimentalBlockType } from '@woocommerce/block-settings'; + +/** + * Internal dependencies + */ +import { Edit, Save } from './edit'; +import metadata from './block.json'; + +registerExperimentalBlockType( metadata, { + icon: { + src: ( + + ), + }, + edit: Edit, + save: Save, +} ); diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/style.scss b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/style.scss new file mode 100644 index 00000000000..4bf8332ddda --- /dev/null +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/style.scss @@ -0,0 +1,72 @@ +.wp-block-woocommerce-cart { + + &.is-loading .wp-block-woocommerce-cart-cross-sells-block { + @include placeholder(); + margin-top: 2em; + min-height: 15em; + + h3 { + display: none; + } + } + + .wp-block-woocommerce-cart-cross-sells-block { + + .cross-sells-product { + display: inline-block; + box-sizing: content-box; + margin-bottom: 2em; + padding-right: 5%; + text-align: center; + vertical-align: top; + width: 30%; + + &:nth-child(3n + 3) { + padding-right: 0; + } + + div { + .wc-block-components-product-name { + font-weight: 400; + } + + .wc-block-components-product-price { + display: block; + } + } + + .wc-block-components-product-button__button { + margin-top: 1em; + } + + .wc-block-components-product-add-to-cart { + justify-content: center; + + .wc-block-components-product-add-to-cart-button:not(.is-link) { + background-color: #eee; + color: #333; + font-weight: 600; + margin-top: 1em; + + &:focus, + &:hover { + background-color: #d5d5d5; + border-color: #d5d5d5; + color: #333; + } + } + } + } + } +} + +@include breakpoint("<480px") { + .wp-block-woocommerce-cart { + .wp-block-woocommerce-cart-cross-sells-block { + .cross-sells-product { + display: block; + width: 100%; + } + } + } +} diff --git a/assets/js/blocks/cart/inner-blocks/cart-items-block/edit.tsx b/assets/js/blocks/cart/inner-blocks/cart-items-block/edit.tsx index 2ac1a66db44..bfad15f3dcc 100644 --- a/assets/js/blocks/cart/inner-blocks/cart-items-block/edit.tsx +++ b/assets/js/blocks/cart/inner-blocks/cart-items-block/edit.tsx @@ -4,6 +4,7 @@ import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; import { Main } from '@woocommerce/base-components/sidebar-layout'; import { innerBlockAreas } from '@woocommerce/blocks-checkout'; +import { isExperimentalBuild } from '@woocommerce/block-settings'; import type { TemplateArray } from '@wordpress/blocks'; /** * Internal dependencies @@ -13,12 +14,19 @@ import { getAllowedBlocks, } from '../../../cart-checkout-shared'; -export const Edit = ( { clientId }: { clientId: string } ): JSX.Element => { +interface Props { + clientId: string; +} + +export const Edit = ( { clientId }: Props ): JSX.Element => { const blockProps = useBlockProps( { className: 'wc-block-cart__main' } ); const allowedBlocks = getAllowedBlocks( innerBlockAreas.CART_ITEMS ); const defaultTemplate = [ [ 'woocommerce/cart-line-items-block', {}, [] ], - ] as TemplateArray; + ...( isExperimentalBuild() + ? [ [ 'woocommerce/cart-cross-sells-block', {}, [] ] ] + : [] ), + ] as unknown as TemplateArray; useForcedLayout( { clientId, diff --git a/assets/js/blocks/cart/inner-blocks/component-metadata.ts b/assets/js/blocks/cart/inner-blocks/component-metadata.ts index cf979e31355..631d2bdcadb 100644 --- a/assets/js/blocks/cart/inner-blocks/component-metadata.ts +++ b/assets/js/blocks/cart/inner-blocks/component-metadata.ts @@ -6,6 +6,8 @@ import EMPTY_CART from './empty-cart-block/block.json'; import CART_ITEMS from './cart-items-block/block.json'; import CART_EXPRESS_PAYMENT from './cart-express-payment-block/block.json'; import CART_LINE_ITEMS from './cart-line-items-block/block.json'; +import CART_CROSS_SELLS from './cart-cross-sells-block/block.json'; +import CART_CROSS_SELLS_PRODUCTS from './cart-cross-sells-products/block.json'; import CART_TOTALS from './cart-totals-block/block.json'; import PROCEED_TO_CHECKOUT from './proceed-to-checkout-block/block.json'; import CART_ACCEPTED_PAYMENT_METHODS from './cart-accepted-payment-methods-block/block.json'; @@ -24,6 +26,8 @@ export default { CART_ITEMS, CART_EXPRESS_PAYMENT, CART_LINE_ITEMS, + CART_CROSS_SELLS, + CART_CROSS_SELLS_PRODUCTS, CART_TOTALS, PROCEED_TO_CHECKOUT, CART_ACCEPTED_PAYMENT_METHODS, diff --git a/assets/js/blocks/cart/inner-blocks/index.tsx b/assets/js/blocks/cart/inner-blocks/index.tsx index 003f6f54634..99df256e16c 100644 --- a/assets/js/blocks/cart/inner-blocks/index.tsx +++ b/assets/js/blocks/cart/inner-blocks/index.tsx @@ -4,6 +4,8 @@ import './filled-cart-block'; import './cart-items-block'; import './cart-line-items-block'; +import './cart-cross-sells-block'; +import './cart-cross-sells-products'; import './cart-totals-block'; import './cart-express-payment-block'; import './proceed-to-checkout-block'; diff --git a/assets/js/blocks/cart/inner-blocks/register-components.ts b/assets/js/blocks/cart/inner-blocks/register-components.ts index bf4ac082f28..5aec48ea60b 100644 --- a/assets/js/blocks/cart/inner-blocks/register-components.ts +++ b/assets/js/blocks/cart/inner-blocks/register-components.ts @@ -2,7 +2,10 @@ * External dependencies */ import { lazy } from '@wordpress/element'; -import { WC_BLOCKS_BUILD_URL } from '@woocommerce/block-settings'; +import { + WC_BLOCKS_BUILD_URL, + isExperimentalBuild, +} from '@woocommerce/block-settings'; import { registerCheckoutBlock } from '@woocommerce/blocks-checkout'; /** @@ -58,6 +61,30 @@ registerCheckoutBlock( { ), } ); +if ( isExperimentalBuild() ) { + registerCheckoutBlock( { + metadata: metadata.CART_CROSS_SELLS, + component: lazy( + () => + import( + /* webpackChunkName: "cart-blocks/cart-cross-sells" */ + './cart-cross-sells-block/frontend' + ) + ), + } ); + + registerCheckoutBlock( { + metadata: metadata.CART_CROSS_SELLS_PRODUCTS, + component: lazy( + () => + import( + /* webpackChunkName: "cart-blocks/cart-cross-sells-products" */ + './cart-cross-sells-products/frontend' + ) + ), + } ); +} + registerCheckoutBlock( { metadata: metadata.CART_TOTALS, component: lazy( diff --git a/assets/js/blocks/cart/style.scss b/assets/js/blocks/cart/style.scss index d3539cf4097..c495532e6cc 100644 --- a/assets/js/blocks/cart/style.scss +++ b/assets/js/blocks/cart/style.scss @@ -21,7 +21,7 @@ table.wc-block-cart-items td { background: none !important; // Remove borders on default themes. border: 0; - margin: 0; + margin: 0 0 2em; } .editor-styles-wrapper table.wc-block-cart-items, diff --git a/assets/js/data/constants.ts b/assets/js/data/constants.ts index 996dbbd8105..14a16efc68b 100644 --- a/assets/js/data/constants.ts +++ b/assets/js/data/constants.ts @@ -7,6 +7,7 @@ export const API_BLOCK_NAMESPACE = 'wc/blocks'; export const EMPTY_CART_COUPONS: [] = []; export const EMPTY_CART_ITEMS: [] = []; +export const EMPTY_CART_CROSS_SELLS: [] = []; export const EMPTY_CART_FEES: [] = []; export const EMPTY_CART_ITEM_ERRORS: [] = []; export const EMPTY_CART_ERRORS: [] = []; diff --git a/assets/js/data/default-states.ts b/assets/js/data/default-states.ts index 8fe8ec2b698..a0345719078 100644 --- a/assets/js/data/default-states.ts +++ b/assets/js/data/default-states.ts @@ -9,6 +9,7 @@ import type { Cart, CartMeta } from '@woocommerce/types'; import { EMPTY_CART_COUPONS, EMPTY_CART_ITEMS, + EMPTY_CART_CROSS_SELLS, EMPTY_CART_FEES, EMPTY_CART_ITEM_ERRORS, EMPTY_CART_ERRORS, @@ -64,6 +65,7 @@ export const defaultCartState: CartState = { items: EMPTY_CART_ITEMS, itemsCount: 0, itemsWeight: 0, + crossSells: EMPTY_CART_CROSS_SELLS, needsShipping: true, needsPayment: false, hasCalculatedShipping: true, diff --git a/assets/js/previews/cart.ts b/assets/js/previews/cart.ts index 4a06a8a0e0b..6852a35ee9b 100644 --- a/assets/js/previews/cart.ts +++ b/assets/js/previews/cart.ts @@ -172,6 +172,228 @@ export const previewCart: CartResponse = { extensions: {}, }, ], + cross_sells: [ + { + id: 1, + name: __( 'Polo', 'woo-gutenberg-products-block' ), + permalink: 'https://example.org', + prices: { + currency_code: 'USD', + currency_symbol: '$', + currency_minor_unit: 2, + currency_decimal_separator: '.', + currency_thousand_separator: ',', + currency_prefix: '$', + currency_suffix: '', + price: displayWithTax ? '24000' : '20000', + regular_price: displayWithTax ? '24000' : '20000', + sale_price: displayWithTax ? '12000' : '10000', + raw_prices: { + precision: 6, + price: displayWithTax ? '24000000' : '20000000', + regular_price: displayWithTax ? '24000000' : '20000000', + sale_price: displayWithTax ? '12000000' : '10000000', + }, + }, + images: [ + { + id: 17, + src: WC_BLOCKS_IMAGE_URL + 'previews/polo.jpg', + thumbnail: WC_BLOCKS_IMAGE_URL + 'previews/polo.jpg', + srcset: '', + sizes: '', + name: '', + alt: '', + }, + ], + average_rating: 4.5, + }, + { + id: 2, + name: __( 'Long Sleeve Tee', 'woo-gutenberg-products-block' ), + permalink: 'https://example.org', + prices: { + currency_code: 'USD', + currency_symbol: '$', + currency_minor_unit: 2, + currency_decimal_separator: '.', + currency_thousand_separator: ',', + currency_prefix: '$', + currency_suffix: '', + price: displayWithTax ? '30000' : '25000', + regular_price: displayWithTax ? '30000' : '25000', + sale_price: displayWithTax ? '30000' : '25000', + raw_prices: { + precision: 6, + price: displayWithTax ? '30000000' : '25000000', + regular_price: displayWithTax ? '30000000' : '25000000', + sale_price: displayWithTax ? '30000000' : '25000000', + }, + }, + images: [ + { + id: 17, + src: WC_BLOCKS_IMAGE_URL + 'previews/long-sleeve-tee.jpg', + thumbnail: + WC_BLOCKS_IMAGE_URL + 'previews/long-sleeve-tee.jpg', + srcset: '', + sizes: '', + name: '', + alt: '', + }, + ], + average_rating: 4, + }, + { + id: 3, + name: __( 'Hoodie with Zipper', 'woo-gutenberg-products-block' ), + permalink: 'https://example.org', + on_sale: true, + prices: { + currency_code: 'USD', + currency_symbol: '$', + currency_minor_unit: 2, + currency_decimal_separator: '.', + currency_thousand_separator: ',', + currency_prefix: '$', + currency_suffix: '', + price: displayWithTax ? '15000' : '12500', + regular_price: displayWithTax ? '30000' : '25000', + sale_price: displayWithTax ? '15000' : '12500', + raw_prices: { + precision: 6, + price: displayWithTax ? '15000000' : '12500000', + regular_price: displayWithTax ? '30000000' : '25000000', + sale_price: displayWithTax ? '15000000' : '12500000', + }, + }, + images: [ + { + id: 17, + src: + WC_BLOCKS_IMAGE_URL + 'previews/hoodie-with-zipper.jpg', + thumbnail: + WC_BLOCKS_IMAGE_URL + 'previews/hoodie-with-zipper.jpg', + srcset: '', + sizes: '', + name: '', + alt: '', + }, + ], + average_rating: 1, + }, + { + id: 4, + name: __( 'Hoodie with Logo', 'woo-gutenberg-products-block' ), + permalink: 'https://example.org', + on_sale: false, + prices: { + currency_code: 'USD', + currency_symbol: '$', + currency_minor_unit: 2, + currency_decimal_separator: '.', + currency_thousand_separator: ',', + currency_prefix: '$', + currency_suffix: '', + price: displayWithTax ? '4500' : '4250', + regular_price: displayWithTax ? '4500' : '4250', + sale_price: displayWithTax ? '4500' : '4250', + raw_prices: { + precision: 6, + price: displayWithTax ? '45000000' : '42500000', + regular_price: displayWithTax ? '45000000' : '42500000', + sale_price: displayWithTax ? '45000000' : '42500000', + }, + }, + images: [ + { + id: 17, + src: WC_BLOCKS_IMAGE_URL + 'previews/hoodie-with-logo.jpg', + thumbnail: + WC_BLOCKS_IMAGE_URL + 'previews/hoodie-with-logo.jpg', + srcset: '', + sizes: '', + name: '', + alt: '', + }, + ], + average_rating: 5, + }, + { + id: 5, + name: __( 'Hoodie with Pocket', 'woo-gutenberg-products-block' ), + permalink: 'https://example.org', + on_sale: true, + prices: { + currency_code: 'USD', + currency_symbol: '$', + currency_minor_unit: 2, + currency_decimal_separator: '.', + currency_thousand_separator: ',', + currency_prefix: '$', + currency_suffix: '', + price: displayWithTax ? '3500' : '3250', + regular_price: displayWithTax ? '4500' : '4250', + sale_price: displayWithTax ? '3500' : '3250', + raw_prices: { + precision: 6, + price: displayWithTax ? '35000000' : '32500000', + regular_price: displayWithTax ? '45000000' : '42500000', + sale_price: displayWithTax ? '35000000' : '32500000', + }, + }, + images: [ + { + id: 17, + src: + WC_BLOCKS_IMAGE_URL + 'previews/hoodie-with-pocket.jpg', + thumbnail: + WC_BLOCKS_IMAGE_URL + 'previews/hoodie-with-pocket.jpg', + srcset: '', + sizes: '', + name: '', + alt: '', + }, + ], + average_rating: 3.75, + }, + { + id: 6, + name: __( 'T-Shirt', 'woo-gutenberg-products-block' ), + permalink: 'https://example.org', + on_sale: false, + prices: { + currency_code: 'USD', + currency_symbol: '$', + currency_minor_unit: 2, + currency_decimal_separator: '.', + currency_thousand_separator: ',', + currency_prefix: '$', + currency_suffix: '', + price: displayWithTax ? '1800' : '1500', + regular_price: displayWithTax ? '1800' : '1500', + sale_price: displayWithTax ? '1800' : '1500', + raw_prices: { + precision: 6, + price: displayWithTax ? '1800000' : '1500000', + regular_price: displayWithTax ? '1800000' : '1500000', + sale_price: displayWithTax ? '1800000' : '1500000', + }, + }, + images: [ + { + id: 17, + src: WC_BLOCKS_IMAGE_URL + 'previews/tshirt.jpg', + thumbnail: WC_BLOCKS_IMAGE_URL + 'previews/tshirt.jpg', + srcset: '', + sizes: '', + name: '', + alt: '', + }, + ], + average_rating: 3, + }, + ], fees: [ { id: 'fee', diff --git a/assets/js/shared/context/product-data-context.js b/assets/js/shared/context/product-data-context.js index 3d91f5f5635..a81cc16e913 100644 --- a/assets/js/shared/context/product-data-context.js +++ b/assets/js/shared/context/product-data-context.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; import { createContext, useContext } from '@wordpress/element'; /** @@ -68,6 +67,14 @@ const ProductDataContext = createContext( { export const useProductDataContext = () => useContext( ProductDataContext ); +/** + * This context is used to pass product data down to all children blocks in a given tree. + * + * @param {Object} object A react context object + * @param {any|null} object.product The product data to be passed down + * @param {Object} object.children The product data to be passed down + * @param {boolean} object.isLoading The product data to be passed down + */ export const ProductDataContextProvider = ( { product = null, children, @@ -89,8 +96,3 @@ export const ProductDataContextProvider = ( { ); }; - -ProductDataContextProvider.propTypes = { - children: PropTypes.node, - product: PropTypes.object, -}; diff --git a/assets/js/types/type-defs/cart-response.ts b/assets/js/types/type-defs/cart-response.ts index a28bf1b8391..13e6f583f31 100644 --- a/assets/js/types/type-defs/cart-response.ts +++ b/assets/js/types/type-defs/cart-response.ts @@ -3,6 +3,7 @@ */ import { CurrencyResponse } from './currency'; import type { CartItem } from './cart'; +import type { ProductResponseItem } from './product-response'; export interface CartResponseTotalsItem extends CurrencyResponse { total_discount: string; @@ -170,6 +171,7 @@ export interface CartResponse { items: Array< CartResponseItem >; items_count: number; items_weight: number; + cross_sells: Array< ProductResponseItem >; needs_payment: boolean; needs_shipping: boolean; has_calculated_shipping: boolean; diff --git a/assets/js/types/type-defs/cart.ts b/assets/js/types/type-defs/cart.ts index 9d9aa98dfc9..f80a847835a 100644 --- a/assets/js/types/type-defs/cart.ts +++ b/assets/js/types/type-defs/cart.ts @@ -12,7 +12,10 @@ import { ExtensionsData, } from './cart-response'; -import { ProductResponseItemData } from './product-response'; +import { + ProductResponseItemData, + ProductResponseItem, +} from './product-response'; export interface CurrencyInfo { currency_code: CurrencyCode; @@ -190,6 +193,7 @@ export interface Cart { items: Array< CartItem >; itemsCount: number; itemsWeight: number; + crossSells: Array< ProductResponseItem >; needsPayment: boolean; needsShipping: boolean; hasCalculatedShipping: boolean; diff --git a/assets/js/types/type-defs/hooks.ts b/assets/js/types/type-defs/hooks.ts index 716b8645079..09cead7e6c8 100644 --- a/assets/js/types/type-defs/hooks.ts +++ b/assets/js/types/type-defs/hooks.ts @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { ProductResponseItem } from '@woocommerce/type-defs/product-response'; + /** * Internal dependencies */ @@ -34,6 +39,7 @@ export interface StoreCartCoupon { export interface StoreCart { cartCoupons: CartResponseCoupons; cartItems: Array< CartResponseItem >; + crossSellsProducts: Array< ProductResponseItem >; cartFees: Array< CartResponseFeeItem >; cartItemsCount: number; cartItemsWeight: number; diff --git a/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md b/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md index 29c2b43646d..c1c48bdb0ab 100644 --- a/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md +++ b/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md @@ -35,6 +35,16 @@ The majority of our feature flagging is blocks, this is a list of them: ### Experimental flag +- Cart block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/index.js#L44) | [PHP flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/961c0c476d4228a218859c658c42f9b6eebfdec4/src/BlockTypesController.php#L182)). +- Cart Express Checkout block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/cart-express-payment-block/index.tsx#L13)). +- Cart Items block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/cart-items-block/index.tsx#L13)). +- Cart Line Items block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/cart-line-items-block/index.tsx#L13)). +- Cart Order Summary block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/cart-order-summary-block/index.tsx#L14)). +- Cart Totals block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/cart-totals-block/index.tsx#L13)). +- Cross-Sells block ([JS flag](https://github.com/woocommerce/woocommerce-blocks/blob/84950e6188c451d522a7ce614f77272362575d37/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/index.tsx#L13) | [JS flag](https://github.com/woocommerce/woocommerce-blocks/blob/84950e6188c451d522a7ce614f77272362575d37/assets/js/blocks/cart/inner-blocks/cart-items-block/edit.tsx#L26-L28)) +- Empty Cart block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/empty-cart-block/index.tsx#L13)). +- Filled Cart block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/filled-cart-block/index.tsx#L13)). +- Cart Proceed to checkout block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8516e87bddee6c07a080c934f3d8cc0683adef06/assets/js/blocks/cart-checkout/cart-i2/inner-blocks/proceed-to-checkout-block/index.tsx#L14)). - Single Product block ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/9b76ea7a1680e68cc20bfee01078e43ccfc996bd/assets/js/blocks/single-product/index.js#L43) | [PHP flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/4a1ee97eb97011458174e93e44a9b7ad2f10ca36/src/BlockTypesController.php#L181) | [webpack flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/341be1f56071fbd4b5ff975e8788d65a09512df2/bin/webpack-entries.js#L57-L59)). - ⚛️ Add to cart ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/b3a9753d8b7dae18b36025d09fbff835b8365de0/assets/js/atomic/blocks/product-elements/add-to-cart/index.js#L29-L32)). - ⚛️ Product category list ([JS flag](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/b3a9753d8b7dae18b36025d09fbff835b8365de0/assets/js/atomic/blocks/product-elements/category-list/index.js#L29-L32)). @@ -126,4 +136,3 @@ Current list of events: 🐞 Found a mistake, or have a suggestion? [Leave feedback about this document here.](https://github.com/woocommerce/woocommerce-blocks/issues/new?assignees=&labels=type%3A+documentation&template=--doc-feedback.md&title=Feedback%20on%20./docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md) - diff --git a/docs/third-party-developers/extensibility/rest-api/available-endpoints-to-extend.md b/docs/third-party-developers/extensibility/rest-api/available-endpoints-to-extend.md index 87069fb56e4..597f8e2808d 100644 --- a/docs/third-party-developers/extensibility/rest-api/available-endpoints-to-extend.md +++ b/docs/third-party-developers/extensibility/rest-api/available-endpoints-to-extend.md @@ -2,18 +2,18 @@ ## Table of Contents -- [`wc/store/checkout`](#wcstorecheckout) - - [Passed Parameters](#passed-parameters) - - [Key](#key) -- [`wc/store/cart`](#wcstorecart) - - [Passed Parameters](#passed-parameters-1) - - [Key](#key-1) -- [`wc/store/cart/items`](#wcstorecartitems) - - [Passed Parameters](#passed-parameters-2) - - [Key](#key-2) -- [`wc/store/products`](#wcstoreproducts) - - [Passed Parameters](#passed-parameters-3) - - [Key](#key-3) +- [`wc/store/checkout`](#wcstorecheckout) + - [Passed Parameters](#passed-parameters) + - [Key](#key) +- [`wc/store/cart`](#wcstorecart) + - [Passed Parameters](#passed-parameters-1) + - [Key](#key-1) +- [`wc/store/cart/items`](#wcstorecartitems) + - [Passed Parameters](#passed-parameters-2) + - [Key](#key-2) +- [`wc/store/products`](#wcstoreproducts) + - [Passed Parameters](#passed-parameters-3) + - [Key](#key-3) To see how to add your data to Store API using ExtendSchema, [check this document](./extend-rest-api-add-data.md). This is a list of available endpoints that you can extend. If you want to add a new endpoint, [check this document](./extend-rest-api-new-endpoint.md). @@ -78,4 +78,3 @@ The main products endpoint is extensible via ExtendSchema. The data is available 🐞 Found a mistake, or have a suggestion? [Leave feedback about this document here.](https://github.com/woocommerce/woocommerce-blocks/issues/new?assignees=&labels=type%3A+documentation&template=--doc-feedback.md&title=Feedback%20on%20./docs/third-party-developers/extensibility/rest-api/available-endpoints-to-extend.md) - diff --git a/images/previews/beanie.jpg b/images/previews/beanie.jpg index 09fb0606639..b725e78afde 100644 Binary files a/images/previews/beanie.jpg and b/images/previews/beanie.jpg differ diff --git a/images/previews/collection.jpg b/images/previews/collection.jpg index 9b2051cf510..890f6c5e340 100644 Binary files a/images/previews/collection.jpg and b/images/previews/collection.jpg differ diff --git a/images/previews/hoodie-with-logo.jpg b/images/previews/hoodie-with-logo.jpg new file mode 100644 index 00000000000..9609e3bf856 Binary files /dev/null and b/images/previews/hoodie-with-logo.jpg differ diff --git a/images/previews/hoodie-with-pocket.jpg b/images/previews/hoodie-with-pocket.jpg new file mode 100644 index 00000000000..72c3272d0cb Binary files /dev/null and b/images/previews/hoodie-with-pocket.jpg differ diff --git a/images/previews/hoodie-with-zipper.jpg b/images/previews/hoodie-with-zipper.jpg new file mode 100644 index 00000000000..f9b27c9fd35 Binary files /dev/null and b/images/previews/hoodie-with-zipper.jpg differ diff --git a/images/previews/long-sleeve-tee.jpg b/images/previews/long-sleeve-tee.jpg new file mode 100644 index 00000000000..64c7d9d9e69 Binary files /dev/null and b/images/previews/long-sleeve-tee.jpg differ diff --git a/images/previews/pennant.jpg b/images/previews/pennant.jpg index b2093423750..61f64b7cda8 100644 Binary files a/images/previews/pennant.jpg and b/images/previews/pennant.jpg differ diff --git a/images/previews/polo.jpg b/images/previews/polo.jpg new file mode 100644 index 00000000000..6766cc216c7 Binary files /dev/null and b/images/previews/polo.jpg differ diff --git a/images/previews/tshirt.jpg b/images/previews/tshirt.jpg new file mode 100644 index 00000000000..d775d53efd3 Binary files /dev/null and b/images/previews/tshirt.jpg differ diff --git a/packages/checkout/blocks-registry/types.ts b/packages/checkout/blocks-registry/types.ts index 7bbcbbdb7ee..f752f2910bc 100644 --- a/packages/checkout/blocks-registry/types.ts +++ b/packages/checkout/blocks-registry/types.ts @@ -18,6 +18,7 @@ export enum innerBlockAreas { EMPTY_CART = 'woocommerce/empty-cart-block', FILLED_CART = 'woocommerce/filled-cart-block', CART_ITEMS = 'woocommerce/cart-items-block', + CART_CROSS_SELLS = 'woocommerce/cart-cross-sells-block', CART_TOTALS = 'woocommerce/cart-totals-block', MINI_CART = 'woocommerce/mini-cart-contents', EMPTY_MINI_CART = 'woocommerce/empty-mini-cart-contents-block', diff --git a/storybook/__mocks__/woocommerce-base-hooks.js b/storybook/__mocks__/woocommerce-base-hooks.js index 52ddc9fdfce..54b0a037de9 100644 --- a/storybook/__mocks__/woocommerce-base-hooks.js +++ b/storybook/__mocks__/woocommerce-base-hooks.js @@ -19,6 +19,7 @@ export const useStoreCart = () => ( { cartIsLoading: false, cartErrors: [], cartFees: [], + crossSellsProducts: previewCart.cross_sells, billingAddress: {}, shippingAddress: {}, shippingRates: previewShippingRates,