Add Product Card component (#39173)
This commit is contained in:
commit
e7a57502e4
|
@ -25,7 +25,7 @@ export type CategoryAPIItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function fetchCategories(): Promise< CategoryAPIItem[] > {
|
function fetchCategories(): Promise< CategoryAPIItem[] > {
|
||||||
return fetch( MARKETPLACE_URL + 'wp-json/wccom-extensions/1.0/categories' )
|
return fetch( MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/categories' )
|
||||||
.then( ( response ) => {
|
.then( ( response ) => {
|
||||||
if ( ! response.ok ) {
|
if ( ! response.ok ) {
|
||||||
throw new Error( response.statusText );
|
throw new Error( response.statusText );
|
||||||
|
|
|
@ -1 +1,19 @@
|
||||||
// To keep StyleLint happy. Remove when file contains actual code.
|
@import '../../stylesheets/_variables.scss';
|
||||||
|
|
||||||
|
.woocommerce-marketplace {
|
||||||
|
&__discover {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 40px;
|
||||||
|
padding: 0 $content-spacing-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $breakpoint-medium) {
|
||||||
|
.woocommerce-marketplace {
|
||||||
|
&__discover {
|
||||||
|
padding: 0 $content-spacing-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,41 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import ProductList from '../product-list/product-list';
|
||||||
|
import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions';
|
||||||
import './discover.scss';
|
import './discover.scss';
|
||||||
|
|
||||||
export default function Discover(): JSX.Element {
|
export default function Discover(): JSX.Element | null {
|
||||||
|
const [ productGroups, setProductGroups ] = useState<
|
||||||
|
Array< ProductGroup >
|
||||||
|
>( [] );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
fetchDiscoverPageData().then( ( products: Array< ProductGroup > ) => {
|
||||||
|
setProductGroups( products );
|
||||||
|
} );
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
if ( ! productGroups.length ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupsList = productGroups.flatMap( ( group ) => group );
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketplace__discover">
|
<div className="woocommerce-marketplace__discover">
|
||||||
<h1>Discover Our Favorites</h1>
|
{ groupsList.map( ( groups ) => (
|
||||||
|
<ProductList
|
||||||
|
key={ groups.id }
|
||||||
|
title={ groups.title }
|
||||||
|
products={ groups.items }
|
||||||
|
groupURL={ groups.url }
|
||||||
|
/>
|
||||||
|
) ) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
// To keep StyleLint happy. Remove when file contains actual code.
|
.woocommerce-marketplace {
|
||||||
|
&__extensions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,13 +5,19 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import ProductList from '../product-list/product-list';
|
|
||||||
import './extensions.scss';
|
import './extensions.scss';
|
||||||
|
import CategorySelector from '../category-selector/category-selector';
|
||||||
|
|
||||||
export default function Extensions(): JSX.Element {
|
export default function Extensions(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketplace__extensions">
|
<div className="woocommerce-marketplace__extensions">
|
||||||
<ProductList title="Extensions" />
|
<CategorySelector />
|
||||||
|
<div className="woocommerce-marketplace__product-list-content">
|
||||||
|
<div className="woocommerce-marketplace__extension-card"></div>
|
||||||
|
<div className="woocommerce-marketplace__extension-card"></div>
|
||||||
|
<div className="woocommerce-marketplace__extension-card"></div>
|
||||||
|
<div className="woocommerce-marketplace__extension-card"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
@import '../../stylesheets/_variables.scss';
|
||||||
|
|
||||||
|
.woocommerce-marketplace {
|
||||||
|
&__product-card {
|
||||||
|
padding: $large-gap;
|
||||||
|
border-radius: $grid-unit-05 !important;
|
||||||
|
margin-top: $large-gap;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: grid;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $medium-gap;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
grid-template-rows: auto 1fr 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: $grid-unit-60;
|
||||||
|
height: $grid-unit-60;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: $grid-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $medium-gap;
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
color: $gray-900;
|
||||||
|
font-size: $editor-font-size;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: $large-gap;
|
||||||
|
margin: -4px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__vendor {
|
||||||
|
display: flex;
|
||||||
|
gap: $grid-unit-05;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__vendor a {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price {
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: $grid-unit-05;
|
||||||
|
align-self: stretch;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: $gray-900 !important;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: $medium-gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price-billing {
|
||||||
|
color: $gray-600;
|
||||||
|
font-size: $default-font-size;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: $medium-gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $breakpoint-medium) {
|
||||||
|
.woocommerce-marketplace {
|
||||||
|
&__product-card {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Button, Card } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './product-card.scss';
|
||||||
|
import { Product } from '../product-list-content/product-list-content';
|
||||||
|
import { getAdminSetting } from '../../../../client/utils/admin-settings';
|
||||||
|
|
||||||
|
export interface ProductCardProps {
|
||||||
|
type?: string;
|
||||||
|
product: Product;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProductCard( props: ProductCardProps ): JSX.Element {
|
||||||
|
const { product } = props;
|
||||||
|
const currencySymbol = getAdminSetting( 'currency' )?.symbol;
|
||||||
|
|
||||||
|
let productVendor: string | JSX.Element | null = product?.vendorName;
|
||||||
|
if ( product?.vendorName && product?.vendorUrl ) {
|
||||||
|
productVendor = (
|
||||||
|
<a
|
||||||
|
href={ product.vendorUrl }
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{ product.vendorName }
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="woocommerce-marketplace__product-card">
|
||||||
|
<div className="woocommerce-marketplace__product-card__content">
|
||||||
|
<div className="woocommerce-marketplace__product-card__header">
|
||||||
|
<div className="woocommerce-marketplace__product-card__details">
|
||||||
|
{ product.icon && (
|
||||||
|
<img
|
||||||
|
className="woocommerce-marketplace__product-card__icon"
|
||||||
|
src={ product.icon }
|
||||||
|
alt={ product.title }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<div className="woocommerce-marketplace__product-card__meta">
|
||||||
|
<h2 className="woocommerce-marketplace__product-card__title">
|
||||||
|
{ product.title }
|
||||||
|
</h2>
|
||||||
|
{ productVendor && (
|
||||||
|
<p className="woocommerce-marketplace__product-card__vendor">
|
||||||
|
<span>{ __( 'By ', 'woocommerce' ) }</span>
|
||||||
|
{ productVendor }
|
||||||
|
</p>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="woocommerce-marketplace__product-card__description">
|
||||||
|
{ product.description }
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className="woocommerce-marketplace__product-card__price"
|
||||||
|
href={ product.url }
|
||||||
|
target="_blank"
|
||||||
|
variant="link"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{ product.price === 0 || product.price === '0'
|
||||||
|
? __( 'Free download', 'woocommerce' )
|
||||||
|
: currencySymbol + product.price }
|
||||||
|
</span>
|
||||||
|
<span className="woocommerce-marketplace__product-card__price-billing">
|
||||||
|
{ product.price === 0 || product.price === '0'
|
||||||
|
? ''
|
||||||
|
: __( ' annually', 'woocommerce' ) }
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCard;
|
|
@ -1,7 +1,6 @@
|
||||||
@import '../../stylesheets/_variables.scss';
|
@import '../../stylesheets/_variables.scss';
|
||||||
|
|
||||||
.woocommerce-marketplace {
|
.woocommerce-marketplace {
|
||||||
|
|
||||||
&__product-list-content {
|
&__product-list-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $medium-gap;
|
gap: $medium-gap;
|
||||||
|
@ -32,7 +31,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $breakpoint-xlarge) {
|
@media screen and (min-width: $breakpoint-huge) {
|
||||||
.woocommerce-marketplace {
|
.woocommerce-marketplace {
|
||||||
&__product-list-content {
|
&__product-list-content {
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
|
|
@ -1,19 +1,49 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './product-list-content.scss';
|
import './product-list-content.scss';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @woocommerce/dependency-group
|
||||||
|
import ProductCard from '../product-card/product-card';
|
||||||
|
|
||||||
export default function ProductListContent(): JSX.Element {
|
export interface Product {
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
vendorName: string;
|
||||||
|
vendorUrl: string;
|
||||||
|
icon: string;
|
||||||
|
url: string;
|
||||||
|
price: string | number;
|
||||||
|
productType?: string;
|
||||||
|
averageRating?: number | null;
|
||||||
|
reviewsCount?: number | null;
|
||||||
|
currency?: string;
|
||||||
|
}
|
||||||
|
interface ProductListContentProps {
|
||||||
|
products: Product[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductListContent(
|
||||||
|
props: ProductListContentProps
|
||||||
|
): JSX.Element {
|
||||||
|
const { products } = props;
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketplace__product-list-content">
|
<div className="woocommerce-marketplace__product-list-content">
|
||||||
<div className="woocommerce-marketplace__extension-card"></div>
|
{ products.map( ( product ) => (
|
||||||
<div className="woocommerce-marketplace__extension-card"></div>
|
<ProductCard
|
||||||
<div className="woocommerce-marketplace__extension-card"></div>
|
key={ product.id }
|
||||||
<div className="woocommerce-marketplace__extension-card"></div>
|
type="classic"
|
||||||
|
product={ {
|
||||||
|
title: product.title,
|
||||||
|
icon: product.icon,
|
||||||
|
vendorName: product.vendorName,
|
||||||
|
vendorUrl: product.vendorUrl,
|
||||||
|
price: product.price,
|
||||||
|
url: product.url,
|
||||||
|
description: product.description,
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
) ) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,32 @@
|
||||||
.woo-marketplace__product-list-header {
|
@import '../../stylesheets/_variables.scss';
|
||||||
color: $gray-900;
|
|
||||||
|
|
||||||
h2 {
|
.woocommerce-marketplace {
|
||||||
|
&__product-list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $medium-gap;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__product-list-title {
|
||||||
|
flex: 1 0 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 28px;
|
}
|
||||||
margin-top: 0;
|
|
||||||
|
&__product-list-link {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__product-list-link a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { Link } from '@woocommerce/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
@ -9,16 +10,25 @@ import './product-list-header.scss';
|
||||||
|
|
||||||
interface ProductListHeaderProps {
|
interface ProductListHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
groupURL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductListHeader(
|
export default function ProductListHeader(
|
||||||
props: ProductListHeaderProps
|
props: ProductListHeaderProps
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const { title } = props;
|
const { title, groupURL } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woo-marketplace__product-list-header">
|
<div className="woocommerce-marketplace__product-list-header">
|
||||||
<h2>{ title }</h2>
|
<h2 className="woocommerce-marketplace__product-list-title">
|
||||||
|
{ title }
|
||||||
|
</h2>
|
||||||
|
{ groupURL !== null && (
|
||||||
|
<span className="woocommerce-marketplace__product-list-link">
|
||||||
|
<Link href={ groupURL } target="_blank">
|
||||||
|
{ __( 'See more', 'woocommerce' ) }
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,22 +5,24 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import CategorySelector from '../category-selector/category-selector';
|
import ProductListContent, {
|
||||||
import ProductListContent from '../product-list-content/product-list-content';
|
Product,
|
||||||
|
} from '../product-list-content/product-list-content';
|
||||||
import ProductListHeader from '../product-list-header/product-list-header';
|
import ProductListHeader from '../product-list-header/product-list-header';
|
||||||
|
|
||||||
interface ProductListProps {
|
interface ProductListProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
products: Product[];
|
||||||
|
groupURL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductList( props: ProductListProps ): JSX.Element {
|
export default function ProductList( props: ProductListProps ): JSX.Element {
|
||||||
const { title } = props;
|
const { title, products, groupURL } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketplace__product-list">
|
<div className="woocommerce-marketplace__product-list">
|
||||||
<ProductListHeader title={ title } />
|
<ProductListHeader title={ title } groupURL={ groupURL } />
|
||||||
<CategorySelector />
|
<ProductListContent products={ products } />
|
||||||
<ProductListContent />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,14 @@ $content-spacing-small: $grid-unit-20;
|
||||||
$content-spacing-large: $grid-unit-40;
|
$content-spacing-large: $grid-unit-40;
|
||||||
$content-spacing-small: $medium-gap;
|
$content-spacing-small: $medium-gap;
|
||||||
$content-spacing-large: $xlarge-gap;
|
$content-spacing-large: $xlarge-gap;
|
||||||
|
$content-spacing-xlarge: $grid-unit-60;
|
||||||
$content-max-width: 1600px;
|
$content-max-width: 1600px;
|
||||||
|
|
||||||
// Breakpoints
|
// Breakpoints
|
||||||
$breakpoint-medium: 769px;
|
$breakpoint-medium: 769px;
|
||||||
$breakpoint-large: 1024px;
|
$breakpoint-large: 1024px;
|
||||||
$breakpoint-xlarge: 1500px;
|
$breakpoint-xlarge: 1500px;
|
||||||
|
$breakpoint-huge: 1920px;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
$header-height-desktop: 89px;
|
$header-height-desktop: 89px;
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { Product } from '../components/product-list-content/product-list-content';
|
||||||
|
import { MARKETPLACE_URL } from '../components/constants';
|
||||||
|
|
||||||
|
interface ProductGroup {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
items: Product[];
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data for the discover page from the WooCommerce.com API
|
||||||
|
const fetchDiscoverPageData = async (): Promise< Array< ProductGroup > > => {
|
||||||
|
const fetchUrl = MARKETPLACE_URL + '/wp-json/wccom-extensions/2.0/featured';
|
||||||
|
|
||||||
|
return fetch( fetchUrl )
|
||||||
|
.then( ( response ) => {
|
||||||
|
if ( ! response.ok ) {
|
||||||
|
throw new Error( response.statusText );
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
} )
|
||||||
|
.then( ( json ) => {
|
||||||
|
return json;
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
return [];
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
export { fetchDiscoverPageData, ProductGroup };
|
|
@ -33,6 +33,9 @@ declare global {
|
||||||
cta_label: string;
|
cta_label: string;
|
||||||
tc_url: string;
|
tc_url: string;
|
||||||
};
|
};
|
||||||
|
currency?: {
|
||||||
|
symbol: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue