diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-header/product-list-header.scss b/plugins/woocommerce-admin/client/marketplace/components/product-list-header/product-list-header.scss
index 2aa5d4b0d5d..76b21aae5f5 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-list-header/product-list-header.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-header/product-list-header.scss
@@ -13,6 +13,11 @@
font-size: 20px;
font-style: normal;
font-weight: 500;
+ margin-top: $small-gap;
+ }
+
+ &__product-list-title--extensions {
+ margin-bottom: 0;
}
&__product-list-link {
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..f547b09b937 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,14 +1,9 @@
-/**
- * External dependencies
- */
-
/**
* Internal dependencies
*/
-import ProductListContent, {
- Product,
-} from '../product-list-content/product-list-content';
+import ProductListContent from '../product-list-content/product-list-content';
import ProductListHeader from '../product-list-header/product-list-header';
+import { Product } from './types';
interface ProductListProps {
title: string;
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
new file mode 100644
index 00000000000..910e4d096c8
--- /dev/null
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
@@ -0,0 +1,31 @@
+export 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;
+};
+
+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;
+}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-loader/product-loader.scss b/plugins/woocommerce-admin/client/marketplace/components/product-loader/product-loader.scss
index e33965554bf..560b46cf014 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-loader/product-loader.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-loader/product-loader.scss
@@ -1,6 +1,10 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
+ &__product-loader {
+ margin-top: $grid-unit-20;
+ }
+
&__product-loader-cards {
display: grid;
background: linear-gradient(to right, $gray-0 40%, $gray-5 60%, $gray-0 80%);
diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
index 3932483ef25..7b7411ed3ac 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
@@ -3,60 +3,40 @@
*/
import { __ } from '@wordpress/i18n';
import { Icon, search } from '@wordpress/icons';
-import { useState } from '@wordpress/element';
+import { useEffect, useState } from '@wordpress/element';
+import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import './search.scss';
-import { MARKETPLACE_URL } from '../constants';
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;
-}
-
/**
* 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 query = useQuery();
+
+ useEffect( () => {
+ if ( query.term ) {
+ setSearchTerm( query.term );
+ }
+ }, [ query.term ] );
const runSearch = () => {
- const query = searchTerm.trim();
- if ( ! query ) {
- return [];
- }
+ const term = searchTerm.trim();
+
+ // When the search term changes, we reset the category on purpose.
+ navigateTo( {
+ url: getNewPath( { term, category: null, tab: 'extensions' } ),
+ } );
- const params = build_parameter_string( query, country, locale );
- fetch( marketplaceAPI + '?' + params, {
- method: 'GET',
- } )
- .then( ( response ) => response.json() )
- .then( ( response ) => {
- return response;
- } );
return [];
};
@@ -70,6 +50,7 @@ function Search( props: SearchProps ): JSX.Element {
if ( event.key === 'Enter' ) {
runSearch();
}
+
if ( event.key === 'Escape' ) {
setSearchTerm( '' );
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
index cc4ffc39bd7..0e843a4ecdd 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
@@ -34,9 +34,3 @@
border-bottom: 1px solid $gutenberg-gray-300;
}
}
-
-@media screen and (min-width: $breakpoint-medium) {
- .woocommerce-marketplace__tabs {
- padding: 0 $content-spacing-large;
- }
-}
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..261f5def64f
--- /dev/null
+++ b/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx
@@ -0,0 +1,104 @@
+/**
+ * External dependencies
+ */
+import { useQuery } from '@woocommerce/navigation';
+import { useState, useEffect, createContext } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import {
+ Product,
+ SearchAPIProductType,
+} from '../components/product-list/types';
+import { MARKETPLACE_URL } from '../components/constants';
+
+type ProductListContextType = {
+ productList: Product[];
+ isLoading: boolean;
+};
+
+export const ProductListContext = createContext< ProductListContextType >( {
+ productList: [],
+ isLoading: false,
+} );
+
+type ProductListContextProviderProps = {
+ children: JSX.Element;
+ country?: string;
+ locale?: string;
+};
+
+export function ProductListContextProvider(
+ props: ProductListContextProviderProps
+): JSX.Element {
+ const [ isLoading, setIsLoading ] = useState( false );
+ const [ productList, setProductList ] = useState< Product[] >( [] );
+
+ const contextValue = {
+ productList,
+ isLoading,
+ };
+
+ const query = useQuery();
+
+ useEffect( () => {
+ setIsLoading( true );
+
+ const params = new URLSearchParams();
+
+ params.append( 'term', query.term ?? '' );
+ params.append( 'country', props.country ?? '' );
+ params.append( 'locale', props.locale ?? '' );
+
+ if ( query.category ) {
+ params.append( 'category', query.category );
+ }
+
+ const wccomSearchEndpoint =
+ MARKETPLACE_URL +
+ '/wp-json/wccom-extensions/1.0/search?' +
+ params.toString();
+
+ // Fetch data from WCCOM API
+ fetch( wccomSearchEndpoint )
+ .then( ( response ) => response.json() )
+ .then( ( response ) => {
+ /**
+ * Product card component expects a Product type.
+ * So we build that object from the API 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 );
+ } )
+ .catch( () => {
+ setProductList( [] );
+ } )
+ .finally( () => {
+ setIsLoading( false );
+ } );
+ }, [ query, 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 (
-
-
-
-
+
+
+
+
+
+
);
}
diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
index c285c4610e9..1d67664f654 100644
--- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
@@ -1,8 +1,9 @@
/**
* Internal dependencies
*/
-import { Product } from '../components/product-list-content/product-list-content';
+import { Product } from '../components/product-list/types';
import { MARKETPLACE_URL } from '../components/constants';
+import { CategoryAPIItem } from '../components/category-selector/types';
interface ProductGroup {
id: number;
@@ -30,4 +31,21 @@ const fetchDiscoverPageData = async (): Promise< Array< ProductGroup > > => {
} );
};
-export { fetchDiscoverPageData, ProductGroup };
+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 { fetchDiscoverPageData, fetchCategories, ProductGroup };