khaihihi
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { Disabled } from '@wordpress/components';
|
||||
import Gridicon from 'gridicons';
|
||||
import { ProductButton } from '@woocommerce/atomic-components/product';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sharedConfig from '../shared-config';
|
||||
|
||||
const blockConfig = {
|
||||
title: __( 'Product Button', 'woocommerce' ),
|
||||
description: __(
|
||||
'Display a call to action button which either adds the product to the cart, or links to the product page.',
|
||||
'woocommerce'
|
||||
),
|
||||
icon: {
|
||||
src: <Gridicon icon="cart" />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
edit( props ) {
|
||||
const { attributes } = props;
|
||||
|
||||
return (
|
||||
<Disabled>
|
||||
<ProductButton product={ attributes.product } />
|
||||
</Disabled>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-button', {
|
||||
...sharedConfig,
|
||||
...blockConfig,
|
||||
} );
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import Gridicon from 'gridicons';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { Disabled, PanelBody, ToggleControl } from '@wordpress/components';
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import ToggleButtonControl from '@woocommerce/block-components/toggle-button-control';
|
||||
import { ProductImage } from '@woocommerce/atomic-components/product';
|
||||
import { previewProducts } from '@woocommerce/resource-previews';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sharedConfig from '../shared-config';
|
||||
|
||||
const blockConfig = {
|
||||
title: __( 'Product Image', 'woocommerce' ),
|
||||
description: __(
|
||||
'Display the main product image',
|
||||
'woocommerce'
|
||||
),
|
||||
icon: {
|
||||
src: <Gridicon icon="image" />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
attributes: {
|
||||
product: {
|
||||
type: 'object',
|
||||
default: previewProducts[ 0 ],
|
||||
},
|
||||
productLink: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
showSaleBadge: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
saleBadgeAlign: {
|
||||
type: 'string',
|
||||
default: 'right',
|
||||
},
|
||||
},
|
||||
edit( props ) {
|
||||
const { attributes, setAttributes } = props;
|
||||
const { productLink, showSaleBadge, saleBadgeAlign } = attributes;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Content',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Link to Product Page',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={ __(
|
||||
'Links the image to the single product listing.',
|
||||
'woocommerce'
|
||||
) }
|
||||
checked={ productLink }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
productLink: ! productLink,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Show On-Sale Badge',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={ __(
|
||||
'Overlay a "sale" badge if the product is on-sale.',
|
||||
'woocommerce'
|
||||
) }
|
||||
checked={ showSaleBadge }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
showSaleBadge: ! showSaleBadge,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
{ showSaleBadge && (
|
||||
<ToggleButtonControl
|
||||
label={ __(
|
||||
'Sale Badge Alignment',
|
||||
'woocommerce'
|
||||
) }
|
||||
value={ saleBadgeAlign }
|
||||
options={ [
|
||||
{
|
||||
label: __(
|
||||
'Left',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'left',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Center',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'center',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Right',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'right',
|
||||
},
|
||||
] }
|
||||
onChange={ ( value ) =>
|
||||
setAttributes( { saleBadgeAlign: value } )
|
||||
}
|
||||
/>
|
||||
) }
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<Disabled>
|
||||
<ProductImage
|
||||
product={ attributes.product }
|
||||
productLink={ productLink }
|
||||
showSaleBadge={ showSaleBadge }
|
||||
saleBadgeAlign={ saleBadgeAlign }
|
||||
/>
|
||||
</Disabled>
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-image', {
|
||||
...sharedConfig,
|
||||
...blockConfig,
|
||||
} );
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as ProductTitle } from './title';
|
||||
export { default as ProductPrice } from './price';
|
||||
export { default as ProductImage } from './image';
|
||||
export { default as ProductRating } from './rating';
|
||||
export { default as ProductButton } from './button';
|
||||
export { default as ProductSummary } from './summary';
|
||||
export { default as ProductSaleBadge } from './sale-badge';
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import Gridicon from 'gridicons';
|
||||
import { ProductPrice } from '@woocommerce/atomic-components/product';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sharedConfig from '../shared-config';
|
||||
|
||||
const blockConfig = {
|
||||
title: __( 'Product Price', 'woocommerce' ),
|
||||
description: __(
|
||||
'Display the price of a product.',
|
||||
'woocommerce'
|
||||
),
|
||||
icon: {
|
||||
src: <Gridicon icon="money" />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
edit( props ) {
|
||||
const { attributes } = props;
|
||||
|
||||
return <ProductPrice product={ attributes.product } />;
|
||||
},
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-price', {
|
||||
...sharedConfig,
|
||||
...blockConfig,
|
||||
} );
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import Gridicon from 'gridicons';
|
||||
import { ProductRating } from '@woocommerce/atomic-components/product';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sharedConfig from '../shared-config';
|
||||
|
||||
const blockConfig = {
|
||||
title: __( 'Product Rating', 'woocommerce' ),
|
||||
description: __(
|
||||
'Display the average rating of a product.',
|
||||
'woocommerce'
|
||||
),
|
||||
icon: {
|
||||
src: <Gridicon icon="star-outline" />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
edit( props ) {
|
||||
const { attributes } = props;
|
||||
|
||||
return <ProductRating product={ attributes.product } />;
|
||||
},
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-rating', {
|
||||
...sharedConfig,
|
||||
...blockConfig,
|
||||
} );
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { ProductSaleBadge } from '@woocommerce/atomic-components/product';
|
||||
import { IconProductOnSale } from '@woocommerce/block-components/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sharedConfig from '../shared-config';
|
||||
|
||||
const blockConfig = {
|
||||
title: __( 'On-Sale Badge', 'woocommerce' ),
|
||||
description: __(
|
||||
'Displays an on-sale badge if the product is on-sale.',
|
||||
'woocommerce'
|
||||
),
|
||||
icon: {
|
||||
src: <IconProductOnSale />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
supports: {
|
||||
html: false,
|
||||
},
|
||||
edit( props ) {
|
||||
const { align, product } = props.attributes;
|
||||
|
||||
return <ProductSaleBadge product={ product } align={ align } />;
|
||||
},
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-sale-badge', {
|
||||
...sharedConfig,
|
||||
...blockConfig,
|
||||
} );
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Gridicon from 'gridicons';
|
||||
import { previewProducts } from '@woocommerce/resource-previews';
|
||||
|
||||
/**
|
||||
* Holds default config for this collection of blocks.
|
||||
*/
|
||||
export default {
|
||||
category: 'woocommerce',
|
||||
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
|
||||
icon: {
|
||||
src: <Gridicon icon="grid" />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
supports: {
|
||||
html: false,
|
||||
},
|
||||
parent: [ 'woocommerce/all-products' ],
|
||||
attributes: {
|
||||
product: {
|
||||
type: 'object',
|
||||
default: previewProducts[ 0 ],
|
||||
},
|
||||
},
|
||||
save() {},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import Gridicon from 'gridicons';
|
||||
import { ProductSummary } from '@woocommerce/atomic-components/product';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sharedConfig from '../shared-config';
|
||||
|
||||
const blockConfig = {
|
||||
title: __( 'Product Summary', 'woocommerce' ),
|
||||
description: __(
|
||||
'Display the short description of a product.',
|
||||
'woocommerce'
|
||||
),
|
||||
icon: {
|
||||
src: <Gridicon icon="aside" />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
edit( props ) {
|
||||
const { attributes } = props;
|
||||
|
||||
return <ProductSummary product={ attributes.product } />;
|
||||
},
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-summary', {
|
||||
...sharedConfig,
|
||||
...blockConfig,
|
||||
} );
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { Fragment } from 'react';
|
||||
import { Disabled, PanelBody, ToggleControl } from '@wordpress/components';
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import { ProductTitle } from '@woocommerce/atomic-components/product';
|
||||
import { previewProducts } from '@woocommerce/resource-previews';
|
||||
import HeadingToolbar from '@woocommerce/block-components/heading-toolbar';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sharedConfig from '../shared-config';
|
||||
|
||||
const blockConfig = {
|
||||
title: __( 'Product Title', 'woocommerce' ),
|
||||
description: __(
|
||||
'Display the name of a product.',
|
||||
'woocommerce'
|
||||
),
|
||||
icon: {
|
||||
src: 'heading',
|
||||
foreground: '#96588a',
|
||||
},
|
||||
attributes: {
|
||||
product: {
|
||||
type: 'object',
|
||||
default: previewProducts[ 0 ],
|
||||
},
|
||||
headingLevel: {
|
||||
type: 'number',
|
||||
default: 2,
|
||||
},
|
||||
productLink: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
edit( props ) {
|
||||
const { attributes, setAttributes } = props;
|
||||
const { headingLevel, productLink } = attributes;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Content',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
<p>{ __( 'Level', 'woocommerce' ) }</p>
|
||||
<HeadingToolbar
|
||||
isCollapsed={ false }
|
||||
minLevel={ 2 }
|
||||
maxLevel={ 7 }
|
||||
selectedLevel={ headingLevel }
|
||||
onChange={ ( newLevel ) =>
|
||||
setAttributes( { headingLevel: newLevel } )
|
||||
}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Link to Product Page',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={ __(
|
||||
'Links the image to the single product listing.',
|
||||
'woocommerce'
|
||||
) }
|
||||
checked={ productLink }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
productLink: ! productLink,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<Disabled>
|
||||
<ProductTitle
|
||||
headingLevel={ headingLevel }
|
||||
productLink={ productLink }
|
||||
product={ attributes.product }
|
||||
/>
|
||||
</Disabled>
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-title', {
|
||||
...sharedConfig,
|
||||
...blockConfig,
|
||||
} );
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { _n, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { find } from 'lodash';
|
||||
import { useCollection } from '@woocommerce/base-hooks';
|
||||
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* A custom hook for exposing cart related data for a given product id and an
|
||||
* action for adding a single quantity of the product _to_ the cart.
|
||||
*
|
||||
* Currently this is internal only to the ProductButton component until we have
|
||||
* a clearer idea of the pattern that should emerge for a cart hook.
|
||||
*
|
||||
* @param {number} productId The product id for the product connection to the
|
||||
* cart.
|
||||
*
|
||||
* @return {Object} Returns an object with the following properties:
|
||||
* @type {number} cartQuantity The quantity of the product currently in
|
||||
* the cart.
|
||||
* @type {bool} addingToCart Whether the product is currently being
|
||||
* added to the cart (true).
|
||||
* @type {bool} cartIsLoading Whether the cart is being loaded.
|
||||
* @type {Function} addToCart An action dispatcher for adding a single
|
||||
* quantity of the product to the cart.
|
||||
* Receives no arguments, it operates on the
|
||||
* current product.
|
||||
*/
|
||||
const useAddToCart = ( productId ) => {
|
||||
const { results: cartResults, isLoading: cartIsLoading } = useCollection( {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'cart/items',
|
||||
} );
|
||||
const currentCartResults = useRef( null );
|
||||
const { __experimentalPersistItemToCollection } = useDispatch( storeKey );
|
||||
const cartQuantity = useMemo( () => {
|
||||
const productItem = find( cartResults, { id: productId } );
|
||||
return productItem ? productItem.quantity : 0;
|
||||
}, [ cartResults, productId ] );
|
||||
const [ addingToCart, setAddingToCart ] = useState( false );
|
||||
const addToCart = useCallback( () => {
|
||||
setAddingToCart( true );
|
||||
// exclude this item from the cartResults for adding to the new
|
||||
// collection (so it's updated correctly!)
|
||||
const collection = cartResults.filter( ( cartItem ) => {
|
||||
return cartItem.id !== productId;
|
||||
} );
|
||||
__experimentalPersistItemToCollection(
|
||||
'/wc/store',
|
||||
'cart/items',
|
||||
collection,
|
||||
{ id: productId, quantity: 1 }
|
||||
);
|
||||
}, [ productId, cartResults ] );
|
||||
useEffect( () => {
|
||||
if ( currentCartResults.current !== cartResults ) {
|
||||
if ( addingToCart ) {
|
||||
setAddingToCart( false );
|
||||
}
|
||||
currentCartResults.current = cartResults;
|
||||
}
|
||||
}, [ cartResults, addingToCart ] );
|
||||
return {
|
||||
cartQuantity,
|
||||
addingToCart,
|
||||
cartIsLoading,
|
||||
addToCart,
|
||||
};
|
||||
};
|
||||
|
||||
const Event = window.Event || {};
|
||||
|
||||
const ProductButton = ( { product, className } ) => {
|
||||
const {
|
||||
id,
|
||||
permalink,
|
||||
add_to_cart: productCartDetails,
|
||||
has_options: hasOptions,
|
||||
is_purchasable: isPurchasable,
|
||||
is_in_stock: isInStock,
|
||||
} = product;
|
||||
const {
|
||||
cartQuantity,
|
||||
addingToCart,
|
||||
cartIsLoading,
|
||||
addToCart,
|
||||
} = useAddToCart( id );
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
const addedToCart = cartQuantity > 0;
|
||||
const firstMount = useRef( true );
|
||||
const getButtonText = () => {
|
||||
if ( Number.isFinite( cartQuantity ) && addedToCart ) {
|
||||
return sprintf(
|
||||
// translators: %s number of products in cart.
|
||||
_n(
|
||||
'%d in cart',
|
||||
'%d in cart',
|
||||
cartQuantity,
|
||||
'woocommerce'
|
||||
),
|
||||
cartQuantity
|
||||
);
|
||||
}
|
||||
return decodeEntities( productCartDetails.text );
|
||||
};
|
||||
|
||||
// This is a hack to trigger cart updates till we migrate to block based card
|
||||
// that relies on the store, see
|
||||
// https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1247
|
||||
useEffect( () => {
|
||||
if ( firstMount.current ) {
|
||||
firstMount.current = false;
|
||||
return;
|
||||
}
|
||||
// Test if we have our Event defined
|
||||
if ( Object.entries( Event ).length !== 0 ) {
|
||||
const event = new Event( 'wc_fragment_refresh', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
} );
|
||||
document.body.dispatchEvent( event );
|
||||
} else {
|
||||
const event = document.createEvent( 'Event' );
|
||||
event.initEvent( 'wc_fragment_refresh', true, true );
|
||||
document.body.dispatchEvent( event );
|
||||
}
|
||||
}, [ cartQuantity ] );
|
||||
|
||||
const wrapperClasses = classnames(
|
||||
className,
|
||||
`${ layoutStyleClassPrefix }__product-add-to-cart`,
|
||||
'wp-block-button'
|
||||
);
|
||||
|
||||
const buttonClasses = classnames(
|
||||
'wp-block-button__link',
|
||||
'add_to_cart_button',
|
||||
{
|
||||
loading: addingToCart,
|
||||
added: addedToCart,
|
||||
}
|
||||
);
|
||||
|
||||
if ( Object.keys( product ).length === 0 || cartIsLoading ) {
|
||||
return (
|
||||
<div className={ wrapperClasses }>
|
||||
<button className={ buttonClasses } disabled={ true } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const allowAddToCart = ! hasOptions && isPurchasable && isInStock;
|
||||
return (
|
||||
<div className={ wrapperClasses }>
|
||||
{ allowAddToCart ? (
|
||||
<button
|
||||
onClick={ addToCart }
|
||||
aria-label={ decodeEntities(
|
||||
productCartDetails.description
|
||||
) }
|
||||
className={ buttonClasses }
|
||||
disabled={ addingToCart }
|
||||
>
|
||||
{ getButtonText() }
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={ permalink }
|
||||
aria-label={ decodeEntities(
|
||||
productCartDetails.description
|
||||
) }
|
||||
className={ buttonClasses }
|
||||
rel="nofollow"
|
||||
>
|
||||
{ getButtonText() }
|
||||
</a>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
product: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ProductButton;
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { Fragment, useState } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { PLACEHOLDER_IMG_SRC } from '@woocommerce/block-settings';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductSaleBadge } from '../../../components/product';
|
||||
|
||||
const SaleBadge = ( { product, saleBadgeAlign, shouldRender } ) => {
|
||||
return shouldRender ? (
|
||||
<ProductSaleBadge product={ product } align={ saleBadgeAlign } />
|
||||
) : null;
|
||||
};
|
||||
|
||||
const Image = ( { layoutPrefix, loaded, image, onLoad } ) => {
|
||||
const cssClass = classnames( `${ layoutPrefix }__product-image__image`, {
|
||||
[ `${ layoutPrefix }__product-image__image_placeholder` ]:
|
||||
! loaded && ! image,
|
||||
} );
|
||||
const { thumbnail, srcset, sizes, alt } = image || {};
|
||||
return (
|
||||
<Fragment>
|
||||
{ image && (
|
||||
<img
|
||||
className={ cssClass }
|
||||
src={ thumbnail }
|
||||
srcSet={ srcset }
|
||||
sizes={ sizes }
|
||||
alt={ alt }
|
||||
onLoad={ onLoad }
|
||||
hidden={ ! loaded }
|
||||
/>
|
||||
) }
|
||||
{ ! loaded && (
|
||||
<img
|
||||
className={ cssClass }
|
||||
src={ PLACEHOLDER_IMG_SRC }
|
||||
alt=""
|
||||
/>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductImage = ( {
|
||||
className,
|
||||
product,
|
||||
productLink = true,
|
||||
showSaleBadge = true,
|
||||
saleBadgeAlign = 'right',
|
||||
} ) => {
|
||||
const [ imageLoaded, setImageLoaded ] = useState( false );
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
const image =
|
||||
product.images && product.images.length ? product.images[ 0 ] : null;
|
||||
|
||||
const renderedSalesAndImage = (
|
||||
<Fragment>
|
||||
<SaleBadge
|
||||
product={ product }
|
||||
saleBadgeAlign={ saleBadgeAlign }
|
||||
shouldRender={ showSaleBadge }
|
||||
/>
|
||||
<Image
|
||||
layoutPrefix={ layoutStyleClassPrefix }
|
||||
loaded={ imageLoaded }
|
||||
image={ image }
|
||||
onLoad={ () => setImageLoaded( true ) }
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
className,
|
||||
`${ layoutStyleClassPrefix }__product-image`
|
||||
) }
|
||||
>
|
||||
{ productLink ? (
|
||||
<a href={ product.permalink } rel="nofollow">
|
||||
{ renderedSalesAndImage }
|
||||
</a>
|
||||
) : (
|
||||
renderedSalesAndImage
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductImage.propTypes = {
|
||||
className: PropTypes.string,
|
||||
product: PropTypes.object.isRequired,
|
||||
productLink: PropTypes.bool,
|
||||
showSaleBadge: PropTypes.bool,
|
||||
saleBadgeAlign: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ProductImage;
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as ProductButton } from './button';
|
||||
export { default as ProductImage } from './image';
|
||||
export { default as ProductRating } from './rating';
|
||||
export { default as ProductTitle } from './title';
|
||||
export { default as ProductPrice } from './price';
|
||||
export { default as ProductSummary } from './summary';
|
||||
export { default as ProductSaleBadge } from './sale-badge';
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import NumberFormat from 'react-number-format';
|
||||
import classnames from 'classnames';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
|
||||
const ProductPrice = ( { className, product } ) => {
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
const prices = product.prices || {};
|
||||
const numberFormatArgs = {
|
||||
displayType: 'text',
|
||||
thousandSeparator: prices.thousand_separator,
|
||||
decimalSeparator: prices.decimal_separator,
|
||||
decimalScale: prices.decimals,
|
||||
fixedDecimalScale: true,
|
||||
prefix: prices.price_prefix,
|
||||
suffix: prices.price_suffix,
|
||||
};
|
||||
|
||||
if (
|
||||
prices.price_range &&
|
||||
prices.price_range.min_amount &&
|
||||
prices.price_range.max_amount
|
||||
) {
|
||||
const minAmount = parseFloat( prices.price_range.min_amount );
|
||||
const maxAmount = parseFloat( prices.price_range.max_amount );
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
className,
|
||||
`${ layoutStyleClassPrefix }__product-price`
|
||||
) }
|
||||
>
|
||||
<span
|
||||
className={ `${ layoutStyleClassPrefix }__product-price__value` }
|
||||
>
|
||||
<NumberFormat value={ minAmount } { ...numberFormatArgs } />
|
||||
—
|
||||
<NumberFormat value={ maxAmount } { ...numberFormatArgs } />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
className,
|
||||
`${ layoutStyleClassPrefix }__product-price`
|
||||
) }
|
||||
>
|
||||
{ prices.regular_price !== prices.price && (
|
||||
<del
|
||||
className={ `${ layoutStyleClassPrefix }__product-price__regular` }
|
||||
>
|
||||
<NumberFormat
|
||||
value={ prices.regular_price }
|
||||
{ ...numberFormatArgs }
|
||||
/>
|
||||
</del>
|
||||
) }
|
||||
<span
|
||||
className={ `${ layoutStyleClassPrefix }__product-price__value` }
|
||||
>
|
||||
<NumberFormat value={ prices.price } { ...numberFormatArgs } />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductPrice;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
|
||||
const ProductRating = ( { className, product } ) => {
|
||||
const rating = parseFloat( product.average_rating );
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
|
||||
if ( ! Number.isFinite( rating ) || rating === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const starStyle = {
|
||||
width: ( rating / 5 ) * 100 + '%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
className,
|
||||
`${ layoutStyleClassPrefix }__product-rating`
|
||||
) }
|
||||
>
|
||||
<div
|
||||
className={ `${ layoutStyleClassPrefix }__product-rating__stars` }
|
||||
role="img"
|
||||
>
|
||||
<span style={ starStyle }>
|
||||
{ sprintf(
|
||||
__(
|
||||
'Rated %d out of 5',
|
||||
'woocommerce'
|
||||
),
|
||||
rating
|
||||
) }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductRating.propTypes = {
|
||||
className: PropTypes.string,
|
||||
product: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ProductRating;
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
|
||||
const ProductSaleBadge = ( { className, product, align } ) => {
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
const alignClass =
|
||||
typeof align === 'string'
|
||||
? `${ layoutStyleClassPrefix }__product-onsale--align${ align }`
|
||||
: '';
|
||||
|
||||
if ( product && product.on_sale ) {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
className,
|
||||
alignClass,
|
||||
`${ layoutStyleClassPrefix }__product-onsale`
|
||||
) }
|
||||
>
|
||||
{ __( 'Sale', 'woocommerce' ) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ProductSaleBadge;
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
|
||||
const ProductSummary = ( { className, product } ) => {
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
if ( ! product.description ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
className,
|
||||
`${ layoutStyleClassPrefix }__product-summary`
|
||||
) }
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: product.description,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProductSummary.propTypes = {
|
||||
className: PropTypes.string,
|
||||
product: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ProductSummary;
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
const ProductTitle = ( {
|
||||
className,
|
||||
product,
|
||||
headingLevel = 2,
|
||||
productLink = true,
|
||||
} ) => {
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
if ( ! product.name ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const productName = decodeEntities( product.name );
|
||||
const TagName = `h${ headingLevel }`;
|
||||
|
||||
return (
|
||||
<TagName
|
||||
className={ classnames(
|
||||
className,
|
||||
`${ layoutStyleClassPrefix }__product-title`
|
||||
) }
|
||||
>
|
||||
{ productLink ? (
|
||||
<a href={ product.permalink } rel="nofollow">
|
||||
{ productName }
|
||||
</a>
|
||||
) : (
|
||||
productName
|
||||
) }
|
||||
</TagName>
|
||||
);
|
||||
};
|
||||
|
||||
ProductTitle.propTypes = {
|
||||
className: PropTypes.string,
|
||||
product: PropTypes.object.isRequired,
|
||||
headingLevel: PropTypes.number,
|
||||
productLink: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ProductTitle;
|
||||
Reference in New Issue
Block a user