From bb84e90ef460d94bacccbe589da932a6e355a617 Mon Sep 17 00:00:00 2001 From: raicem Date: Tue, 8 Aug 2023 09:34:46 +0300 Subject: [PATCH] Marketplace: Add category selector --- .../category-selector/category-dropdown.tsx | 69 ++++++++++ .../category-selector/category-link.tsx | 21 +++ .../category-selector/category-selector.scss | 109 +++++++++++++++ .../category-selector/category-selector.tsx | 125 ++++++++++++++++++ .../marketplace/components/constants.ts | 1 + .../marketplace/components/footer/footer.tsx | 7 +- .../header-account/header-account.tsx | 3 +- .../product-list-content.scss | 1 + .../components/product-list/product-list.tsx | 2 + .../marketplace/components/search/search.tsx | 4 +- .../marketplace/stylesheets/_variables.scss | 2 + 11 files changed, 338 insertions(+), 6 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx create mode 100644 plugins/woocommerce-admin/client/marketplace/components/category-selector/category-link.tsx create mode 100644 plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss create mode 100644 plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx new file mode 100644 index 00000000000..8baf649898b --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx @@ -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 ( + + ); +} + +type CategoryDropdownProps = { + label: string; + categories: Category[]; + className?: string; + buttonClassName?: string; + contentClassName?: string; + arrowIconSize?: number; +}; + +export default function CategoryDropdown( + props: CategoryDropdownProps +): JSX.Element { + return ( + ( + + ) } + className={ props.className } + renderContent={ () => ( + + ) } + contentClassName={ props.contentClassName } + /> + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-link.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-link.tsx new file mode 100644 index 00000000000..0de23b40150 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-link.tsx @@ -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 ; +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss new file mode 100644 index 00000000000..2e66b9d3895 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss @@ -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%; +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx new file mode 100644 index 00000000000..711f9e37231 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx @@ -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' ) } + + + ); + } + + return ( + <> +
    + { firstBatch.map( ( category ) => ( +
  • + +
  • + ) ) } +
  • + +
  • +
+ +
+ +
+ + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/constants.ts b/plugins/woocommerce-admin/client/marketplace/components/constants.ts index a7d504a0f42..548249734e2 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/constants.ts +++ b/plugins/woocommerce-admin/client/marketplace/components/constants.ts @@ -1,2 +1,3 @@ export const DEFAULT_TAB_KEY = 'discover'; export const MARKETPLACE_PATH = '/extensions'; +export const MARKETPLACE_URL = 'https://woocommerce.com'; diff --git a/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx b/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx index a6e23d073a2..96517f99699 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx @@ -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 money back guarantee', 'woocommerce' ), { // eslint-disable-next-line jsx-a11y/anchor-has-content - a: , + a: , } ); @@ -25,7 +26,7 @@ const supportTitle = createInterpolateElement( __( 'Support teams across the world', 'woocommerce' ), { // eslint-disable-next-line jsx-a11y/anchor-has-content - a: , + a: , } ); @@ -33,7 +34,7 @@ const paymentTitle = createInterpolateElement( __( 'Safe & Secure online payment', 'woocommerce' ), { // eslint-disable-next-line jsx-a11y/anchor-has-content - a: , + a: , } ); diff --git a/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.tsx b/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.tsx index 17b06939777..63c6ea8cc7f 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.tsx @@ -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 = () => { diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss index 7ef7a8b7a84..bbbfefdb9c7 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss @@ -5,6 +5,7 @@ &__product-list-content { display: grid; gap: $medium-gap; + margin-top: $grid-unit-20; } &__extension-card { diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list/product-list.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list/product-list.tsx index 0aaca9de173..8537915f64a 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list/product-list.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list/product-list.tsx @@ -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 (
+
); diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx index ed1d6742655..3932483ef25 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx @@ -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'; diff --git a/plugins/woocommerce-admin/client/marketplace/stylesheets/_variables.scss b/plugins/woocommerce-admin/client/marketplace/stylesheets/_variables.scss index f1a050234ae..24c2ace259c 100644 --- a/plugins/woocommerce-admin/client/marketplace/stylesheets/_variables.scss +++ b/plugins/woocommerce-admin/client/marketplace/stylesheets/_variables.scss @@ -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;