[wccom-17942] Only showing feedback snackbar when content of in-app marketplace has finished loading. Making sure snackbar is fixed position, so it's visible wherever you are on the page.

- `ProductListContextProvider` provides `setIsLoading` function as well as `isLoading`.
- `Discover` uses these values from context, instead of keeping a loading state in itself.
- `FeedbackModal` calls `maybSetTimeout` when `isLoading` changes. If `isLoading` isn't truthy, and snackbar hasn't already rendered, it sets a timeout of 5 seconds to show it.

- Removed wrapping <WooFooterItem> from around Footer component, so it's no longer a child of the WooCommerce Admin `.woocommerce-layout__footer` footer.
- Removed the `position: relative` from `.woocommerce-layout__footer`. It needs to be `position: fixed`.
- Added FooterContent component to Footer, to allow the layout we want.

- Changed use of context. This now only has states for the selected tab and loading state.
- We use this context in `Tabs` and `Content` to keep track of which tab is selected, and set the selected tab.
- We also use it in `Discover` and `Extensions`, which both report loading state to the context. This allows us to use it to only render the snackbar when loading is complete.
- Extensions: moved `productList` and `setProductList` and logic for getting product list from the context provider to a state in this component. We don't need to share the list of products in the context.
- Renamed `ProductListContext`, `ProductListContextProvider` and `productListContextValue` to more generic `MarketplaceContext`, `MarketplaceContextProvider` and `marketplaceContextValue`.
- Renamed a constant and created constants for API paths.
- Only shows snackbar after content has loaded, and after a timeout. We set a date `marketplace_redesign_2023_last_shown_date` in local storage to ensure we only show one snackbar.
This commit is contained in:
And Finally 2023-08-23 16:49:36 +01:00
parent 4dc745cc37
commit 3c8f4861e4
15 changed files with 224 additions and 210 deletions

View File

@ -1,3 +1,5 @@
export const DEFAULT_TAB_KEY = 'discover'; export const DEFAULT_TAB_KEY = 'discover';
export const MARKETPLACE_API_HOST = 'https://woocommerce.com';
export const MARKETPLACE_PATH = '/extensions'; export const MARKETPLACE_PATH = '/extensions';
export const MARKETPLACE_URL = 'https://woocommerce.com'; export const MARKETPLACE_SEARCH_API_PATH = '/wp-json/wccom-extensions/1.0/search';
export const MARKETPLACE_CATEGORY_API_PATH = '/wp-json/wccom-extensions/1.0/categories';

View File

@ -1,6 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { useContext } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
@ -8,12 +9,7 @@
import './content.scss'; import './content.scss';
import Discover from '../discover/discover'; import Discover from '../discover/discover';
import Extensions from '../extensions/extensions'; import Extensions from '../extensions/extensions';
import Footer from '../footer/footer'; import { MarketplaceContext } from '../../contexts/marketplace-context';
import FeedbackModal from '../feedback-modal/feedback-modal';
export interface ContentProps {
selectedTab?: string | undefined;
}
const renderContent = ( selectedTab?: string ): JSX.Element => { const renderContent = ( selectedTab?: string ): JSX.Element => {
switch ( selectedTab ) { switch ( selectedTab ) {
@ -24,15 +20,12 @@ const renderContent = ( selectedTab?: string ): JSX.Element => {
} }
}; };
export default function Content( props: ContentProps ): JSX.Element { export default function Content(): JSX.Element {
const { selectedTab } = props; const marketplaceContextValue = useContext( MarketplaceContext );
const { selectedTab } = marketplaceContextValue;
return ( return (
<> <div className="woocommerce-marketplace__content">
<div className="woocommerce-marketplace__content"> { renderContent( selectedTab ) }
{ renderContent( selectedTab ) } </div>
</div>
<Footer />
<FeedbackModal />
</>
); );
} }

View File

@ -1,7 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { useEffect, useState } from '@wordpress/element'; import { useContext, useEffect, useState } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
@ -9,14 +9,17 @@ import { useEffect, useState } from '@wordpress/element';
import ProductList from '../product-list/product-list'; import ProductList from '../product-list/product-list';
import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions'; import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions';
import ProductLoader from '../product-loader/product-loader'; import ProductLoader from '../product-loader/product-loader';
import { MarketplaceContext } from '../../contexts/marketplace-context';
import './discover.scss'; import './discover.scss';
export default function Discover(): JSX.Element | null { export default function Discover(): JSX.Element | null {
const [ productGroups, setProductGroups ] = useState< const [ productGroups, setProductGroups ] = useState<
Array< ProductGroup > Array< ProductGroup >
>( [] ); >( [] );
const [ isLoading, setIsLoading ] = useState( false ); const marketplaceContextValue = useContext( MarketplaceContext );
const { isLoading, setIsLoading } = marketplaceContextValue;
// Get the content for this screen
useEffect( () => { useEffect( () => {
setIsLoading( true ); setIsLoading( true );

View File

@ -1,7 +1,8 @@
/** /**
* External dependencies * External dependencies
*/ */
import { useContext } from '@wordpress/element'; import { useContext, useEffect, useState } from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
import { __, _n, sprintf } from '@wordpress/i18n'; import { __, _n, sprintf } from '@wordpress/i18n';
/** /**
@ -9,15 +10,78 @@ import { __, _n, sprintf } from '@wordpress/i18n';
*/ */
import './extensions.scss'; import './extensions.scss';
import CategorySelector from '../category-selector/category-selector'; import CategorySelector from '../category-selector/category-selector';
import { ProductListContext } from '../../contexts/product-list-context'; import { MarketplaceContext } from '../../contexts/marketplace-context';
import ProductListContent from '../product-list-content/product-list-content'; import ProductListContent from '../product-list-content/product-list-content';
import ProductLoader from '../product-loader/product-loader'; import ProductLoader from '../product-loader/product-loader';
import NoResults from '../product-list-content/no-results'; import NoResults from '../product-list-content/no-results';
import {
Product,
SearchAPIProductType,
} from '../product-list/types';
import { MARKETPLACE_SEARCH_API_PATH, MARKETPLACE_API_HOST } from '../constants';
export default function Extensions(): JSX.Element { export default function Extensions(): JSX.Element {
const productListContextValue = useContext( ProductListContext ); const [ productList, setProductList ] = useState<Product[]>( [] );
const marketplaceContextValue = useContext( MarketplaceContext );
const { isLoading, setIsLoading } = marketplaceContextValue;
const query = useQuery();
// Get the content for this screen
useEffect( () => {
setIsLoading( true );
const params = new URLSearchParams();
if ( query.term ) {
params.append( 'term', query.term );
}
if ( query.category ) {
params.append( 'category', query.category );
}
const wccomSearchEndpoint =
MARKETPLACE_API_HOST +
MARKETPLACE_SEARCH_API_PATH + '?' +
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,
// Due to backwards compatibility, raw_price is from search API, price is from featured API
price: product.raw_price ?? product.price,
averageRating: product.rating ?? 0,
reviewsCount: product.reviews_count ?? 0,
};
}
);
setProductList( products );
} )
.catch( () => {
setProductList( [] );
} )
.finally( () => {
setIsLoading( false );
} );
}, [ query ] );
const { productList, isLoading } = productListContextValue;
const products = productList.slice( 0, 60 ); const products = productList.slice( 0, 60 );
let title = __( '0 extensions found', 'woocommerce' ); let title = __( '0 extensions found', 'woocommerce' );

View File

@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Modal, Button, TextareaControl } from '@wordpress/components'; import { Modal, Button, TextareaControl } from '@wordpress/components';
import { useDispatch } from '@wordpress/data'; import { useDispatch } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element'; import { useContext, useEffect, useState } from '@wordpress/element';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
/** /**
@ -12,15 +12,24 @@ import { recordEvent } from '@woocommerce/tracks';
*/ */
import './feedback-modal.scss'; import './feedback-modal.scss';
import LikertScale from '../likert-scale/likert-scale'; import LikertScale from '../likert-scale/likert-scale';
import { MarketplaceContext } from '../../contexts/marketplace-context';
export default function FeedbackModal(): JSX.Element { export default function FeedbackModal(): JSX.Element {
const CUSTOMER_EFFORT_SCORE_ACTION = 'marketplace_redesign_2023'; const CUSTOMER_EFFORT_SCORE_ACTION = 'marketplace_redesign_2023';
const LOCALSTORAGE_KEY_DISMISSAL_COUNT = const LOCALSTORAGE_KEY_DISMISSAL_COUNT =
'marketplace_redesign_2023_dismissals'; // ensure we don't ask for feedback if the user's already given feedback or declined to multiple times 'marketplace_redesign_2023_dismissals'; // Ensure we don't ask for feedback if the
// user's already given feedback or declined to multiple times
const LOCALSTORAGE_KEY_LAST_REQUESTED_DATE = const LOCALSTORAGE_KEY_LAST_REQUESTED_DATE =
'marketplace_redesign_2023_last_shown_date'; // ensure we don't ask for feedback more than once per day 'marketplace_redesign_2023_last_shown_date'; // Ensure we don't ask for feedback more
const SUPPRESS_IF_DISMISSED_X_TIMES = 1; // if the user dismisses the snackbar this many times, stop asking for feedback // than once per day
const SUPPRESS_IF_AFTER_DATE = '2024-01-01'; // if this date is reached, stop asking for feedback const SUPPRESS_IF_DISMISSED_X_TIMES = 1; // If the user dismisses the snackbar this many
// times, stop asking for feedback
const SUPPRESS_IF_AFTER_DATE = '2024-01-01'; // If this date is reached, stop asking for
// feedback
const SNACKBAR_TIMEOUT = 5000; // How long we wait before asking for feedback
const marketplaceContextValue = useContext( MarketplaceContext );
const { isLoading } = marketplaceContextValue;
// Save that we dismissed the dialog or snackbar TODAY so we don't show it again until tomorrow (if ever) // Save that we dismissed the dialog or snackbar TODAY so we don't show it again until tomorrow (if ever)
const dismissToday = () => const dismissToday = () =>
@ -74,12 +83,19 @@ export default function FeedbackModal(): JSX.Element {
const { createNotice } = useDispatch( 'core/notices' ); const { createNotice } = useDispatch( 'core/notices' );
function maybeShowSnackbar() { function maybeShowSnackbar() {
// don't show if the user has already given feedback or otherwise suppressed: let timer: ReturnType<typeof setTimeout> | undefined;
// Don't show if we're still loading content
if ( isLoading ) {
return;
}
// Don't show if the user has already given feedback or otherwise suppressed
if ( isDismissedForever() ) { if ( isDismissedForever() ) {
return; return;
} }
// don't show if the user has already declined to provide feedback today: // Don't show if we've already shown today or user has declined today
const today = new Date().toDateString(); const today = new Date().toDateString();
if ( if (
today === today ===
@ -88,6 +104,15 @@ export default function FeedbackModal(): JSX.Element {
return; return;
} }
timer = setTimeout( showSnackbar, SNACKBAR_TIMEOUT );
// Without this, navigating between screens will create a series of snackbars
dismissToday();
return () => { clearTimeout( timer ); }
}
function showSnackbar() {
createNotice( createNotice(
'success', 'success',
__( 'How easy is it to find an extension?', 'woocommerce' ), __( 'How easy is it to find an extension?', 'woocommerce' ),
@ -142,8 +167,7 @@ export default function FeedbackModal(): JSX.Element {
); );
} }
// eslint-disable-next-line react-hooks/exhaustive-deps -- [] => we only want this effect to run once, on first render useEffect( maybeShowSnackbar, [ isLoading ] );
useEffect( maybeShowSnackbar, [] );
// We don't want the "How easy was it to find an extension?" dialog to appear forever: // We don't want the "How easy was it to find an extension?" dialog to appear forever:
const FEEDBACK_DIALOG_CAN_APPEAR = const FEEDBACK_DIALOG_CAN_APPEAR =

View File

@ -1,47 +1,47 @@
@import '../../stylesheets/_variables.scss'; @import '../../stylesheets/_variables.scss';
.woocommerce-admin-page__extensions .woocommerce-layout__footer {
background: #f6f7f7;
// Undo default fixed footer style used in WC Admin
position: relative;
width: 100%;
}
.woocommerce-marketplace__footer { .woocommerce-marketplace__footer {
box-sizing: content-box; background: $gray-0;
max-width: $content-max-width; border-top: 1px solid $gray-200;
margin: auto; margin: auto;
padding: $content-spacing-xlarge $content-spacing-small; padding: $content-spacing-xlarge $content-spacing-small;
&-title {
color: $gutenberg-gray-900;
font-size: 20px;
font-style: normal;
font-weight: 500;
line-height: 28px;
max-width: 389px;
margin: 0 0 $content-spacing-large;
}
a { a {
text-decoration: none; text-decoration: none;
} }
}
&-columns { .woocommerce-marketplace__footer-content {
display: flex; box-sizing: content-box;
flex-direction: column; max-width: $content-max-width;
gap: $large-gap; margin: auto;
} width: 100%;
}
&-logo { .woocommerce-marketplace__footer-title {
color: $wp-gray-50; color: $gutenberg-gray-900;
display: flex; font-size: 20px;
font-size: 14px; font-style: normal;
font-weight: 600; font-weight: 500;
line-height: 20px; line-height: 28px;
gap: $small-gap; max-width: 389px;
margin: 48px 0 0; margin: 0 0 $content-spacing-large;
} }
.woocommerce-marketplace__footer-columns {
display: flex;
flex-direction: column;
gap: $large-gap;
}
.woocommerce-marketplace__footer-logo {
color: $wp-gray-50;
display: flex;
font-size: 14px;
font-weight: 600;
line-height: 20px;
gap: $small-gap;
margin: 48px 0 0;
} }
@media screen and (min-width: $breakpoint-medium) { @media screen and (min-width: $breakpoint-medium) {

View File

@ -12,13 +12,13 @@ import { createInterpolateElement } from '@wordpress/element';
import './footer.scss'; import './footer.scss';
import IconWithText from '../icon-with-text/icon-with-text'; import IconWithText from '../icon-with-text/icon-with-text';
import WooIcon from '../../assets/images/woo-icon.svg'; import WooIcon from '../../assets/images/woo-icon.svg';
import { MARKETPLACE_URL } from '../constants'; import { MARKETPLACE_API_HOST } from '../constants';
const refundPolicyTitle = createInterpolateElement( const refundPolicyTitle = createInterpolateElement(
__( '30 day <a>money back guarantee</a>', 'woocommerce' ), __( '30 day <a>money back guarantee</a>', 'woocommerce' ),
{ {
// eslint-disable-next-line jsx-a11y/anchor-has-content // eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={ MARKETPLACE_URL + '/refund-policy/' } />, a: <a href={ MARKETPLACE_API_HOST + '/refund-policy/' } />,
} }
); );
@ -26,7 +26,7 @@ const supportTitle = createInterpolateElement(
__( '<a>Get help</a> when you need it', 'woocommerce' ), __( '<a>Get help</a> when you need it', 'woocommerce' ),
{ {
// eslint-disable-next-line jsx-a11y/anchor-has-content // eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={ MARKETPLACE_URL + '/docs/' } />, a: <a href={ MARKETPLACE_API_HOST + '/docs/' } />,
} }
); );
@ -34,13 +34,13 @@ const paymentTitle = createInterpolateElement(
__( '<a>Products</a> you can trust', 'woocommerce' ), __( '<a>Products</a> you can trust', 'woocommerce' ),
{ {
// eslint-disable-next-line jsx-a11y/anchor-has-content // eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={ MARKETPLACE_URL + '/products/' } />, a: <a href={ MARKETPLACE_API_HOST + '/products/' } />,
} }
); );
function FooterContent(): JSX.Element { function FooterContent(): JSX.Element {
return ( return (
<div className="woocommerce-marketplace__footer"> <div className="woocommerce-marketplace__footer-content">
<h2 className="woocommerce-marketplace__footer-title"> <h2 className="woocommerce-marketplace__footer-title">
{ __( { __(
'Hundreds of vetted products and services. Unlimited potential.', 'Hundreds of vetted products and services. Unlimited potential.',
@ -83,8 +83,8 @@ function FooterContent(): JSX.Element {
export default function Footer(): JSX.Element { export default function Footer(): JSX.Element {
return ( return (
<WooFooterItem> <div className="woocommerce-marketplace__footer">
<FooterContent /> <FooterContent />
</WooFooterItem> </div>
); );
} }

View File

@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n';
import './header-account.scss'; import './header-account.scss';
import { getAdminSetting } from '../../../utils/admin-settings'; import { getAdminSetting } from '../../../utils/admin-settings';
import HeaderAccountModal from './header-account-modal'; import HeaderAccountModal from './header-account-modal';
import { MARKETPLACE_URL } from '../constants'; import { MARKETPLACE_API_HOST } from '../constants';
export default function HeaderAccount(): JSX.Element { export default function HeaderAccount(): JSX.Element {
const [ isModalOpen, setIsModalOpen ] = useState( false ); const [ isModalOpen, setIsModalOpen ] = useState( false );
@ -28,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. // 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. // 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountURL: any = MARKETPLACE_URL + '/my-dashboard/'; const accountURL: any = MARKETPLACE_API_HOST + '/my-dashboard/';
const accountOrConnect = isConnected ? accountURL : connectionURL; const accountOrConnect = isConnected ? accountURL : connectionURL;
const avatar = () => { const avatar = () => {

View File

@ -1,7 +1,3 @@
/**
* External dependencies
*/
/** /**
* Internal dependencies * Internal dependencies
*/ */
@ -11,13 +7,7 @@ import HeaderAccount from '../header-account/header-account';
import Tabs from '../tabs/tabs'; import Tabs from '../tabs/tabs';
import Search from '../search/search'; import Search from '../search/search';
export interface HeaderProps { export default function Header() {
selectedTab?: string | undefined;
setSelectedTab: ( value: string ) => void;
}
export default function Header( props: HeaderProps ) {
const { selectedTab, setSelectedTab } = props;
return ( return (
<header className="woocommerce-marketplace__header"> <header className="woocommerce-marketplace__header">
<HeaderTitle /> <HeaderTitle />
@ -25,8 +15,6 @@ export default function Header( props: HeaderProps ) {
additionalClassNames={ [ additionalClassNames={ [
'woocommerce-marketplace__header-tabs', 'woocommerce-marketplace__header-tabs',
] } ] }
selectedTab={ selectedTab }
setSelectedTab={ setSelectedTab }
/> />
<Search /> <Search />
<div className="woocommerce-marketplace__header-meta"> <div className="woocommerce-marketplace__header-meta">

View File

@ -2,7 +2,7 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useEffect } from '@wordpress/element'; import { useContext, useEffect } from '@wordpress/element';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import classNames from 'classnames'; import classNames from 'classnames';
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation'; import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
@ -12,10 +12,10 @@ import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
*/ */
import './tabs.scss'; import './tabs.scss';
import { DEFAULT_TAB_KEY, MARKETPLACE_PATH } from '../constants'; import { DEFAULT_TAB_KEY, MARKETPLACE_PATH } from '../constants';
import { MarketplaceContext } from '../../contexts/marketplace-context';
import { MarketplaceContextType } from '../../contexts/types';
export interface TabsProps { export interface TabsProps {
selectedTab?: string | undefined;
setSelectedTab: ( value: string ) => void;
additionalClassNames?: Array< string > | undefined; additionalClassNames?: Array< string > | undefined;
} }
@ -63,8 +63,8 @@ const setUrlTabParam = ( tabKey: string ) => {
} ); } );
}; };
const renderTabs = ( props: TabsProps ) => { const renderTabs = ( contextValue: MarketplaceContextType ) => {
const { selectedTab, setSelectedTab } = props; const { selectedTab, setSelectedTab } = contextValue;
const tabContent = []; const tabContent = [];
for ( const tabKey in tabs ) { for ( const tabKey in tabs ) {
tabContent.push( tabContent.push(
@ -104,14 +104,16 @@ const renderTabs = ( props: TabsProps ) => {
}; };
const Tabs = ( props: TabsProps ): JSX.Element => { const Tabs = ( props: TabsProps ): JSX.Element => {
const { setSelectedTab, additionalClassNames } = props; const { additionalClassNames } = props;
const marketplaceContextValue = useContext( MarketplaceContext );
const { setSelectedTab } = marketplaceContextValue;
interface Query { interface Query {
path?: string; path?: string;
tab?: string; tab?: string;
} }
const query: Query = useQuery(); const query: Record<string, string> = useQuery();
useEffect( () => { useEffect( () => {
if ( query?.tab && tabs[ query.tab ] ) { if ( query?.tab && tabs[ query.tab ] ) {
@ -128,7 +130,7 @@ const Tabs = ( props: TabsProps ): JSX.Element => {
additionalClassNames || [] additionalClassNames || []
) } ) }
> >
{ renderTabs( props ) } { renderTabs( marketplaceContextValue ) }
</nav> </nav>
); );
}; };

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { useState, createContext } from '@wordpress/element';
/**
* Internal dependencies
*/
import { DEFAULT_TAB_KEY } from '../components/constants';
import { MarketplaceContextType } from './types';
export const MarketplaceContext = createContext< MarketplaceContextType >( {
isLoading: false,
setIsLoading: () => {},
selectedTab: DEFAULT_TAB_KEY,
setSelectedTab: () => {},
} );
export function MarketplaceContextProvider( props: {
children: JSX.Element;
} ): JSX.Element {
const [ isLoading, setIsLoading ] = useState( true );
const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY );
const contextValue = {
isLoading,
setIsLoading,
selectedTab,
setSelectedTab
};
return (
<MarketplaceContext.Provider value={ contextValue }>
{ props.children }
</MarketplaceContext.Provider>
);
}

View File

@ -1,98 +0,0 @@
/**
* 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,
} );
export function ProductListContextProvider( props: {
children: JSX.Element;
} ): 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();
if ( query.term ) {
params.append( 'term', query.term );
}
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,
// Due to backwards compatibility, raw_price is from search API, price is from featured API
price: product.raw_price ?? product.price,
averageRating: product.rating ?? 0,
reviewsCount: product.reviews_count ?? 0,
};
}
);
setProductList( products );
} )
.catch( () => {
setProductList( [] );
} )
.finally( () => {
setIsLoading( false );
} );
}, [ query ] );
return (
<ProductListContext.Provider value={ contextValue }>
{ props.children }
</ProductListContext.Provider>
);
}

View File

@ -0,0 +1,6 @@
export type MarketplaceContextType = {
isLoading: boolean;
setIsLoading: ( isLoading: boolean ) => void;
selectedTab: string;
setSelectedTab: ( tab: string ) => void;
};

View File

@ -1,29 +1,22 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './marketplace.scss'; import './marketplace.scss';
import { DEFAULT_TAB_KEY } from './components/constants'; import { MarketplaceContextProvider } from './contexts/marketplace-context';
import Header from './components/header/header'; import Header from './components/header/header';
import Content from './components/content/content'; import Content from './components/content/content';
import { ProductListContextProvider } from './contexts/product-list-context'; import Footer from './components/footer/footer';
import FeedbackModal from './components/feedback-modal/feedback-modal';
export default function Marketplace() { export default function Marketplace() {
const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY );
return ( return (
<ProductListContextProvider> <MarketplaceContextProvider>
<div className="woocommerce-marketplace"> <div className="woocommerce-marketplace">
<Header <Header />
selectedTab={ selectedTab } <Content />
setSelectedTab={ setSelectedTab } <FeedbackModal />
/> <Footer />
<Content selectedTab={ selectedTab } />
</div> </div>
</ProductListContextProvider> </MarketplaceContextProvider>
); );
} }

View File

@ -7,7 +7,7 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies * Internal dependencies
*/ */
import { Product } from '../components/product-list/types'; import { Product } from '../components/product-list/types';
import { MARKETPLACE_URL } from '../components/constants'; import { MARKETPLACE_API_HOST, MARKETPLACE_CATEGORY_API_PATH } from '../components/constants';
import { CategoryAPIItem } from '../components/category-selector/types'; import { CategoryAPIItem } from '../components/category-selector/types';
import { LOCALE } from '../../utils/admin-settings'; import { LOCALE } from '../../utils/admin-settings';
@ -34,7 +34,7 @@ async function fetchDiscoverPageData(): Promise< ProductGroup[] > {
} }
function fetchCategories(): Promise< CategoryAPIItem[] > { function fetchCategories(): Promise< CategoryAPIItem[] > {
let url = MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/categories'; let url = MARKETPLACE_API_HOST + MARKETPLACE_CATEGORY_API_PATH;
if ( LOCALE.userLocale ) { if ( LOCALE.userLocale ) {
url = `${ url }?locale=${ LOCALE.userLocale }`; url = `${ url }?locale=${ LOCALE.userLocale }`;