Marketplace: Category selector UI (#39561)
This commit is contained in:
commit
85b4011c13
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Dropdown } from '@wordpress/components';
|
||||
import { chevronDown, chevronUp, Icon } from '@wordpress/icons';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Category } from './category-selector';
|
||||
|
||||
function DropdownContent( props: {
|
||||
readonly categories: Category[];
|
||||
} ): JSX.Element {
|
||||
return (
|
||||
<ul className="woocommerce-marketplace__category-dropdown-list">
|
||||
{ props.categories.map( ( category ) => (
|
||||
<li
|
||||
className="woocommerce-marketplace__category-dropdown-item"
|
||||
key={ category.slug }
|
||||
>
|
||||
<button className="woocommerce-marketplace__category-dropdown-item-button">
|
||||
{ category.label }
|
||||
</button>
|
||||
</li>
|
||||
) ) }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
type CategoryDropdownProps = {
|
||||
label: string;
|
||||
categories: Category[];
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
contentClassName?: string;
|
||||
arrowIconSize?: number;
|
||||
};
|
||||
|
||||
export default function CategoryDropdown(
|
||||
props: CategoryDropdownProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<Dropdown
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<button
|
||||
onClick={ onToggle }
|
||||
className={ props.buttonClassName }
|
||||
aria-label={ __(
|
||||
'Toggle category dropdown',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
{ props.label }
|
||||
<Icon
|
||||
icon={ isOpen ? chevronUp : chevronDown }
|
||||
size={ props.arrowIconSize }
|
||||
/>
|
||||
</button>
|
||||
) }
|
||||
className={ props.className }
|
||||
renderContent={ () => (
|
||||
<DropdownContent categories={ props.categories } />
|
||||
) }
|
||||
contentClassName={ props.contentClassName }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Category } from './category-selector';
|
||||
|
||||
export default function CategoryLink( props: Category ): JSX.Element {
|
||||
const classes = classNames(
|
||||
'woocommerce-marketplace__category-item-button',
|
||||
{
|
||||
'woocommerce-marketplace__category-item-button--selected':
|
||||
props.selected,
|
||||
}
|
||||
);
|
||||
|
||||
return <button className={ classes }>{ props.label }</button>;
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
@import '../../stylesheets/_variables.scss';
|
||||
|
||||
.woocommerce-marketplace__category-selector {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-item {
|
||||
cursor: pointer;
|
||||
|
||||
.components-dropdown {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-item-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: $wp-gray-60;
|
||||
background-color: $wp-gray-0;
|
||||
padding: 6px $grid-unit-10;
|
||||
margin-right: $grid-unit-10;
|
||||
line-height: 20px;
|
||||
height: 100%;
|
||||
|
||||
&--selected {
|
||||
color: $white;
|
||||
background-color: $gray-900;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-item-content {
|
||||
.components-popover__content {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-selector--full-width {
|
||||
display: none;
|
||||
margin-top: $grid-unit-15;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-medium) {
|
||||
.woocommerce-marketplace__category-selector--full-width {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-selector {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-dropdown-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
border: 1px solid $gray-600;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
padding: $grid-unit-15 $grid-unit-10;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-dropdown-content {
|
||||
background-color: $white;
|
||||
color: $gray-900;
|
||||
font-size: 13px;
|
||||
min-width: 280px;
|
||||
width: calc(100% - 32px);
|
||||
|
||||
.components-popover__content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-dropdown-list {
|
||||
margin: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-dropdown-item {
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: $gutenberg-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__category-dropdown-item-button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: inherit;
|
||||
color: $gray-900;
|
||||
text-align: left;
|
||||
padding: 6px $grid-unit-10;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Spinner } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CategoryLink from './category-link';
|
||||
import './category-selector.scss';
|
||||
import CategoryDropdown from './category-dropdown';
|
||||
import { MARKETPLACE_URL } from '../constants';
|
||||
|
||||
export type Category = {
|
||||
readonly slug: string;
|
||||
readonly label: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type CategoryAPIItem = {
|
||||
readonly slug: string;
|
||||
readonly label: string;
|
||||
};
|
||||
|
||||
function fetchCategories(): Promise< CategoryAPIItem[] > {
|
||||
return fetch( MARKETPLACE_URL + 'wp-json/wccom-extensions/1.0/categories' )
|
||||
.then( ( response ) => {
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( response.statusText );
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} )
|
||||
.then( ( json ) => {
|
||||
return json;
|
||||
} )
|
||||
.catch( () => {
|
||||
return [];
|
||||
} );
|
||||
}
|
||||
|
||||
export default function CategorySelector(): JSX.Element {
|
||||
const [ firstBatch, setFirstBatch ] = useState< Category[] >( [] );
|
||||
const [ secondBatch, setSecondBatch ] = useState< Category[] >( [] );
|
||||
const [ isLoading, setIsLoading ] = useState( false );
|
||||
|
||||
useEffect( () => {
|
||||
setIsLoading( true );
|
||||
fetchCategories()
|
||||
.then( ( categoriesFromAPI: CategoryAPIItem[] ) => {
|
||||
const categories: Category[] = categoriesFromAPI.map(
|
||||
( categoryAPIItem: CategoryAPIItem ): Category => {
|
||||
return {
|
||||
...categoryAPIItem,
|
||||
selected: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Put the "All" category to the beginning
|
||||
categories.sort( ( a ) => {
|
||||
if ( a.slug === '_all' ) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
} );
|
||||
|
||||
// Split array into two from 7th item
|
||||
const firstBatchCategories = categories.slice( 0, 7 );
|
||||
const secondBatchCategories = categories.slice( 7 );
|
||||
|
||||
setFirstBatch( firstBatchCategories );
|
||||
setSecondBatch( secondBatchCategories );
|
||||
} )
|
||||
.finally( () => {
|
||||
setIsLoading( false );
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<>
|
||||
{ __( 'Loading categories…', 'woocommerce' ) }
|
||||
<Spinner />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="woocommerce-marketplace__category-selector">
|
||||
{ firstBatch.map( ( category ) => (
|
||||
<li
|
||||
className="woocommerce-marketplace__category-item"
|
||||
key={ category.slug }
|
||||
>
|
||||
<CategoryLink { ...category } />
|
||||
</li>
|
||||
) ) }
|
||||
<li className="woocommerce-marketplace__category-item">
|
||||
<CategoryDropdown
|
||||
label={ __( 'More', 'woocommerce' ) }
|
||||
categories={ secondBatch }
|
||||
buttonClassName="woocommerce-marketplace__category-item-button"
|
||||
contentClassName="woocommerce-marketplace__category-item-content"
|
||||
arrowIconSize={ 20 }
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="woocommerce-marketplace__category-selector--full-width">
|
||||
<CategoryDropdown
|
||||
label={ __( 'All Categories', 'woocommerce' ) }
|
||||
categories={ firstBatch.concat( secondBatch ) }
|
||||
buttonClassName="woocommerce-marketplace__category-dropdown-button"
|
||||
className="woocommerce-marketplace__category-dropdown"
|
||||
contentClassName="woocommerce-marketplace__category-dropdown-content"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export const DEFAULT_TAB_KEY = 'discover';
|
||||
export const MARKETPLACE_PATH = '/extensions';
|
||||
export const MARKETPLACE_URL = 'https://woocommerce.com';
|
||||
|
|
|
@ -12,12 +12,13 @@ import { createInterpolateElement } from '@wordpress/element';
|
|||
import './footer.scss';
|
||||
import IconWithText from '../icon-with-text/icon-with-text';
|
||||
import WooIcon from '../../assets/images/woo-icon.svg';
|
||||
import { MARKETPLACE_URL } from '../constants';
|
||||
|
||||
const refundPolicyTitle = createInterpolateElement(
|
||||
__( '30 day <a>money back guarantee</a>', 'woocommerce' ),
|
||||
{
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
a: <a href="https://woocommerce.com/refund-policy/" />,
|
||||
a: <a href={ MARKETPLACE_URL + '/refund-policy/' } />,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -25,7 +26,7 @@ const supportTitle = createInterpolateElement(
|
|||
__( '<a>Support</a> teams across the world', 'woocommerce' ),
|
||||
{
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
a: <a href="https://woocommerce.com/docs/" />,
|
||||
a: <a href={ MARKETPLACE_URL + '/docs/' } />,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -33,7 +34,7 @@ const paymentTitle = createInterpolateElement(
|
|||
__( '<a>Safe & Secure</a> online payment', 'woocommerce' ),
|
||||
{
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
a: <a href="https://woocommerce.com/products/woocommerce-payments/" />,
|
||||
a: <a href={ MARKETPLACE_URL + '/woocommerce-payments/' } />,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { __ } from '@wordpress/i18n';
|
|||
import './header-account.scss';
|
||||
import { getAdminSetting } from '../../../utils/admin-settings';
|
||||
import HeaderAccountModal from './header-account-modal';
|
||||
import { MARKETPLACE_URL } from '../constants';
|
||||
|
||||
export default function HeaderAccount(): JSX.Element {
|
||||
const [ isModalOpen, setIsModalOpen ] = useState( false );
|
||||
|
@ -27,7 +28,7 @@ export default function HeaderAccount(): JSX.Element {
|
|||
// component. That component is either an anchor with href if provided or a button that won't accept an href if no href is provided.
|
||||
// Due to early erroring of TypeScript, it only takes the button version into account which doesn't accept href.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const accountURL: any = 'https://woocommerce.com/my-dashboard/';
|
||||
const accountURL: any = MARKETPLACE_URL + '/my-dashboard/';
|
||||
const accountOrConnect = isConnected ? accountURL : connectionURL;
|
||||
|
||||
const avatar = () => {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
&__product-list-content {
|
||||
display: grid;
|
||||
gap: $medium-gap;
|
||||
margin-top: $grid-unit-20;
|
||||
}
|
||||
|
||||
&__extension-card {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CategorySelector from '../category-selector/category-selector';
|
||||
import ProductListContent from '../product-list-content/product-list-content';
|
||||
import ProductListHeader from '../product-list-header/product-list-header';
|
||||
|
||||
|
@ -18,6 +19,7 @@ export default function ProductList( props: ProductListProps ): JSX.Element {
|
|||
return (
|
||||
<div className="woocommerce-marketplace__product-list">
|
||||
<ProductListHeader title={ title } />
|
||||
<CategorySelector />
|
||||
<ProductListContent />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -9,11 +9,11 @@ import { useState } from '@wordpress/element';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import './search.scss';
|
||||
import { MARKETPLACE_URL } from '../constants';
|
||||
|
||||
const searchPlaceholder = __( 'Search extensions and themes', 'woocommerce' );
|
||||
|
||||
const marketplaceAPI =
|
||||
'https://woocommerce.com/wp-json/wccom-extensions/1.0/search';
|
||||
const marketplaceAPI = MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/search';
|
||||
|
||||
export interface SearchProps {
|
||||
locale?: string | 'en_US';
|
||||
|
|
|
@ -31,4 +31,6 @@ $gutenberg-gray-700: $gray-700;
|
|||
$gutenberg-gray-900: $gray-900;
|
||||
$mauve-light-12: $gray-900;
|
||||
$woo-purple-50: #7f54b3;
|
||||
$wp-gray-0: $gray-0;
|
||||
$wp-gray-50: $gray-50;
|
||||
$wp-gray-60: $gray-60;
|
||||
|
|
Loading…
Reference in New Issue