From 0aa3c2686287c27760b4b90e35a508f5512d51a3 Mon Sep 17 00:00:00 2001 From: raicem Date: Thu, 10 Aug 2023 10:16:56 +0300 Subject: [PATCH 1/2] Marketplace: Add product list context --- .../components/extensions/extensions.tsx | 15 ++- .../product-list-content.scss | 1 + .../product-list-content.tsx | 2 +- .../components/product-list/product-list.tsx | 4 - .../marketplace/components/search/search.tsx | 33 +---- .../contexts/product-list-context.tsx | 116 ++++++++++++++++++ .../client/marketplace/index.tsx | 17 +-- 7 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx diff --git a/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx b/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx index b373015704c..98221d3deea 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx @@ -1,23 +1,26 @@ /** * External dependencies */ +import { useContext } from 'react'; /** * Internal dependencies */ import './extensions.scss'; import CategorySelector from '../category-selector/category-selector'; +import { ProductListContext } from '../../contexts/product-list-context'; +import ProductListContent from '../product-list-content/product-list-content'; export default function Extensions(): JSX.Element { + const productListContextValue = useContext( ProductListContext ); + + let { productList } = productListContextValue; + productList = productList.splice( 0, 21 ); + return (
-
-
-
-
-
-
+
); } 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 869cb402184..0161227a6f5 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 @@ -9,6 +9,7 @@ &__extension-card { background-color: #3c3c3c; + color: $white; height: 270px; } } diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx index 840d21add22..90ba6fa6eb1 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx @@ -2,7 +2,6 @@ * Internal dependencies */ 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 interface Product { @@ -19,6 +18,7 @@ export interface Product { reviewsCount?: number | null; currency?: string; } + interface ProductListContentProps { products: Product[]; } 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 c28188c515b..37b6227da6b 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 @@ -1,7 +1,3 @@ -/** - * External dependencies - */ - /** * Internal dependencies */ diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx index 3932483ef25..a235b4d46df 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx @@ -3,18 +3,16 @@ */ import { __ } from '@wordpress/i18n'; import { Icon, search } from '@wordpress/icons'; -import { useState } from '@wordpress/element'; +import { useContext, useState } from '@wordpress/element'; /** * Internal dependencies */ import './search.scss'; -import { MARKETPLACE_URL } from '../constants'; +import { ProductListContext } from '../../contexts/product-list-context'; const searchPlaceholder = __( 'Search extensions and themes', 'woocommerce' ); -const marketplaceAPI = MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/search'; - export interface SearchProps { locale?: string | 'en_US'; country?: string | undefined; @@ -23,25 +21,12 @@ export interface SearchProps { /** * Search component. * - * @param {SearchProps} props - Search props: locale and country. * @return {JSX.Element} Search component. */ -function Search( props: SearchProps ): JSX.Element { - const locale = props.locale ?? 'en_US'; - const country = props.country ?? ''; +function Search(): JSX.Element { const [ searchTerm, setSearchTerm ] = useState( '' ); - const build_parameter_string = ( - query_string: string, - query_country: string, - query_locale: string - ): string => { - const params = new URLSearchParams(); - params.append( 'term', query_string ); - params.append( 'country', query_country ); - params.append( 'locale', query_locale ); - return params.toString(); - }; + const productListContextValue = useContext( ProductListContext ); const runSearch = () => { const query = searchTerm.trim(); @@ -49,14 +34,8 @@ function Search( props: SearchProps ): JSX.Element { return []; } - const params = build_parameter_string( query, country, locale ); - fetch( marketplaceAPI + '?' + params, { - method: 'GET', - } ) - .then( ( response ) => response.json() ) - .then( ( response ) => { - return response; - } ); + productListContextValue.setSearchTerm( query ); + return []; }; diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx new file mode 100644 index 00000000000..67c704b3fd1 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx @@ -0,0 +1,116 @@ +/** + * External dependencies + */ +import { useState, useEffect, createContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Product } from '../components/product-list-content/product-list-content'; + +type SearchAPIProductType = { + title: string; + image: string; + excerpt: string; + link: string; + demo_url: string; + price: string; + hash: string; + slug: string; + id: number; + rating: number | null; + reviews_count: number | null; + vendor_name: string; + vendor_url: string; + icon: string; +}; + +type ProductListContextType = { + productList: Product[]; + setSearchTerm: ( searchTerm: string ) => void; + setCategory: ( category: string ) => void; + isLoading: boolean; +}; + +export const ProductListContext = createContext< ProductListContextType >( { + productList: [], + setSearchTerm: () => {}, + setCategory: () => {}, + isLoading: false, +} ); + +type ProductListContextProviderProps = { + children: JSX.Element; + country?: string; + locale?: string; +}; + +/** + * Internal dependencies + */ +export function ProductListContextProvider( + props: ProductListContextProviderProps +): JSX.Element { + const [ isLoading, setIsLoading ] = useState( false ); + const [ searchTerm, setSearchTerm ] = useState( '' ); + const [ category, setCategory ] = useState( '' ); + const [ productList, setProductList ] = useState< Product[] >( [] ); + + const contextValue = { + productList, + setSearchTerm, + setCategory, + isLoading, + }; + + useEffect( () => { + setIsLoading( true ); + setProductList( [] ); + + // Build up a query string + const params = new URLSearchParams(); + + params.append( 'term', searchTerm ); + params.append( 'country', props.country ?? '' ); + params.append( 'locale', props.locale ?? '' ); + + const wccomSearchEndpoint = + 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' + + '?' + + params.toString(); + + // Fetch data from WCCOM API + fetch( wccomSearchEndpoint ) + .then( ( response ) => response.json() ) + .then( ( response ) => { + const products = response.products.map( + ( product: SearchAPIProductType ): Product => { + return { + id: product.id, + title: product.title, + description: product.excerpt, + vendorName: product.vendor_name, + vendorUrl: product.vendor_url, + icon: product.icon, + url: product.link, + price: product.price, + averageRating: product.rating ?? 0, + reviewsCount: product.reviews_count ?? 0, + currency: '', + }; + } + ); + + setProductList( products ); + } ) + .finally( () => { + setIsLoading( false ); + } ); + }, [ searchTerm, category, props.country, props.locale ] ); + + return ( + + { props.children } + + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/index.tsx b/plugins/woocommerce-admin/client/marketplace/index.tsx index 2b1e528e9dc..95d6b99d81b 100644 --- a/plugins/woocommerce-admin/client/marketplace/index.tsx +++ b/plugins/woocommerce-admin/client/marketplace/index.tsx @@ -10,17 +10,20 @@ import './marketplace.scss'; import { DEFAULT_TAB_KEY } from './components/constants'; import Header from './components/header/header'; import Content from './components/content/content'; +import { ProductListContextProvider } from './contexts/product-list-context'; export default function Marketplace() { const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY ); return ( -
-
- -
+ +
+
+ +
+
); } From 31aa9ebd59f63df8908a25a31894b2b69a1188fe Mon Sep 17 00:00:00 2001 From: raicem Date: Wed, 9 Aug 2023 16:04:35 +0300 Subject: [PATCH 2/2] Marketplace: add category filtering --- .../category-selector/category-dropdown.tsx | 47 ++++++- .../category-selector/category-link.tsx | 27 +++- .../category-selector/category-selector.scss | 17 +++ .../category-selector/category-selector.tsx | 129 ++++++++++++------ .../components/category-selector/types.ts | 10 ++ .../components/discover/discover.scss | 9 -- .../components/extensions/extensions.tsx | 27 +++- .../components/product-card/product-card.tsx | 2 +- .../product-list-content.tsx | 24 +--- .../product-list-header.scss | 5 + .../components/product-list/product-list.tsx | 5 +- .../components/product-list/types.ts | 31 +++++ .../product-loader/product-loader.scss | 4 + .../marketplace/components/search/search.tsx | 28 ++-- .../marketplace/components/tabs/tabs.scss | 6 - .../contexts/product-list-context.tsx | 58 ++++---- .../client/marketplace/utils/functions.tsx | 22 ++- 17 files changed, 306 insertions(+), 145 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketplace/components/category-selector/types.ts create mode 100644 plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts 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 index 8baf649898b..b4f8065bbc9 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx @@ -4,15 +4,39 @@ import { Dropdown } from '@wordpress/components'; import { chevronDown, chevronUp, Icon } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +import { navigateTo, getNewPath } from '@woocommerce/navigation'; +import classNames from 'classnames'; /** * Internal dependencies */ -import { Category } from './category-selector'; +import { Category } from './types'; function DropdownContent( props: { readonly categories: Category[]; + readonly selected?: Category; + readonly onClick: () => void; } ): JSX.Element { + function updateCategorySelection( + event: React.MouseEvent< HTMLButtonElement > + ) { + const slug = event.currentTarget.value; + + if ( ! slug ) { + return; + } + + /** + * Trigger the onClick event on the parent component to close the dropdown. + * This closes the dropdown automatically when a user clicks on an item. + */ + props.onClick(); + + navigateTo( { + url: getNewPath( { category: slug } ), + } ); + } + return (