[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:
parent
4dc745cc37
commit
3c8f4861e4
|
@ -1,3 +1,5 @@
|
|||
export const DEFAULT_TAB_KEY = 'discover';
|
||||
export const MARKETPLACE_API_HOST = 'https://woocommerce.com';
|
||||
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';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useContext } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -8,12 +9,7 @@
|
|||
import './content.scss';
|
||||
import Discover from '../discover/discover';
|
||||
import Extensions from '../extensions/extensions';
|
||||
import Footer from '../footer/footer';
|
||||
import FeedbackModal from '../feedback-modal/feedback-modal';
|
||||
|
||||
export interface ContentProps {
|
||||
selectedTab?: string | undefined;
|
||||
}
|
||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||
|
||||
const renderContent = ( selectedTab?: string ): JSX.Element => {
|
||||
switch ( selectedTab ) {
|
||||
|
@ -24,15 +20,12 @@ const renderContent = ( selectedTab?: string ): JSX.Element => {
|
|||
}
|
||||
};
|
||||
|
||||
export default function Content( props: ContentProps ): JSX.Element {
|
||||
const { selectedTab } = props;
|
||||
export default function Content(): JSX.Element {
|
||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||
const { selectedTab } = marketplaceContextValue;
|
||||
return (
|
||||
<>
|
||||
<div className="woocommerce-marketplace__content">
|
||||
{ renderContent( selectedTab ) }
|
||||
</div>
|
||||
<Footer />
|
||||
<FeedbackModal />
|
||||
</>
|
||||
<div className="woocommerce-marketplace__content">
|
||||
{ renderContent( selectedTab ) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -9,14 +9,17 @@ import { useEffect, useState } from '@wordpress/element';
|
|||
import ProductList from '../product-list/product-list';
|
||||
import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions';
|
||||
import ProductLoader from '../product-loader/product-loader';
|
||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||
import './discover.scss';
|
||||
|
||||
export default function Discover(): JSX.Element | null {
|
||||
const [ productGroups, setProductGroups ] = useState<
|
||||
Array< ProductGroup >
|
||||
>( [] );
|
||||
const [ isLoading, setIsLoading ] = useState( false );
|
||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||
const { isLoading, setIsLoading } = marketplaceContextValue;
|
||||
|
||||
// Get the content for this screen
|
||||
useEffect( () => {
|
||||
setIsLoading( true );
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
|
@ -9,15 +10,78 @@ import { __, _n, sprintf } from '@wordpress/i18n';
|
|||
*/
|
||||
import './extensions.scss';
|
||||
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 ProductLoader from '../product-loader/product-loader';
|
||||
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 {
|
||||
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 );
|
||||
|
||||
let title = __( '0 extensions found', 'woocommerce' );
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { Modal, Button, TextareaControl } from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
|
@ -12,15 +12,24 @@ import { recordEvent } from '@woocommerce/tracks';
|
|||
*/
|
||||
import './feedback-modal.scss';
|
||||
import LikertScale from '../likert-scale/likert-scale';
|
||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||
|
||||
export default function FeedbackModal(): JSX.Element {
|
||||
const CUSTOMER_EFFORT_SCORE_ACTION = 'marketplace_redesign_2023';
|
||||
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 =
|
||||
'marketplace_redesign_2023_last_shown_date'; // ensure we don't ask for feedback more than once per day
|
||||
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
|
||||
'marketplace_redesign_2023_last_shown_date'; // Ensure we don't ask for feedback more
|
||||
// than once per day
|
||||
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)
|
||||
const dismissToday = () =>
|
||||
|
@ -74,12 +83,19 @@ export default function FeedbackModal(): JSX.Element {
|
|||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
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() ) {
|
||||
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();
|
||||
if (
|
||||
today ===
|
||||
|
@ -88,6 +104,15 @@ export default function FeedbackModal(): JSX.Element {
|
|||
return;
|
||||
}
|
||||
|
||||
timer = setTimeout( showSnackbar, SNACKBAR_TIMEOUT );
|
||||
|
||||
// Without this, navigating between screens will create a series of snackbars
|
||||
dismissToday();
|
||||
|
||||
return () => { clearTimeout( timer ); }
|
||||
}
|
||||
|
||||
function showSnackbar() {
|
||||
createNotice(
|
||||
'success',
|
||||
__( '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, [] );
|
||||
useEffect( maybeShowSnackbar, [ isLoading ] );
|
||||
|
||||
// We don't want the "How easy was it to find an extension?" dialog to appear forever:
|
||||
const FEEDBACK_DIALOG_CAN_APPEAR =
|
||||
|
|
|
@ -1,47 +1,47 @@
|
|||
@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 {
|
||||
box-sizing: content-box;
|
||||
max-width: $content-max-width;
|
||||
background: $gray-0;
|
||||
border-top: 1px solid $gray-200;
|
||||
margin: auto;
|
||||
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 {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-columns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $large-gap;
|
||||
}
|
||||
.woocommerce-marketplace__footer-content {
|
||||
box-sizing: content-box;
|
||||
max-width: $content-max-width;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-logo {
|
||||
color: $wp-gray-50;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
gap: $small-gap;
|
||||
margin: 48px 0 0;
|
||||
}
|
||||
.woocommerce-marketplace__footer-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;
|
||||
}
|
||||
|
||||
.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) {
|
||||
|
|
|
@ -12,13 +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';
|
||||
import { MARKETPLACE_API_HOST } 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={ 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' ),
|
||||
{
|
||||
// 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' ),
|
||||
{
|
||||
// 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 {
|
||||
return (
|
||||
<div className="woocommerce-marketplace__footer">
|
||||
<div className="woocommerce-marketplace__footer-content">
|
||||
<h2 className="woocommerce-marketplace__footer-title">
|
||||
{ __(
|
||||
'Hundreds of vetted products and services. Unlimited potential.',
|
||||
|
@ -83,8 +83,8 @@ function FooterContent(): JSX.Element {
|
|||
|
||||
export default function Footer(): JSX.Element {
|
||||
return (
|
||||
<WooFooterItem>
|
||||
<div className="woocommerce-marketplace__footer">
|
||||
<FooterContent />
|
||||
</WooFooterItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +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';
|
||||
import { MARKETPLACE_API_HOST } from '../constants';
|
||||
|
||||
export default function HeaderAccount(): JSX.Element {
|
||||
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.
|
||||
// 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 = MARKETPLACE_URL + '/my-dashboard/';
|
||||
const accountURL: any = MARKETPLACE_API_HOST + '/my-dashboard/';
|
||||
const accountOrConnect = isConnected ? accountURL : connectionURL;
|
||||
|
||||
const avatar = () => {
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -11,13 +7,7 @@ import HeaderAccount from '../header-account/header-account';
|
|||
import Tabs from '../tabs/tabs';
|
||||
import Search from '../search/search';
|
||||
|
||||
export interface HeaderProps {
|
||||
selectedTab?: string | undefined;
|
||||
setSelectedTab: ( value: string ) => void;
|
||||
}
|
||||
|
||||
export default function Header( props: HeaderProps ) {
|
||||
const { selectedTab, setSelectedTab } = props;
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="woocommerce-marketplace__header">
|
||||
<HeaderTitle />
|
||||
|
@ -25,8 +15,6 @@ export default function Header( props: HeaderProps ) {
|
|||
additionalClassNames={ [
|
||||
'woocommerce-marketplace__header-tabs',
|
||||
] }
|
||||
selectedTab={ selectedTab }
|
||||
setSelectedTab={ setSelectedTab }
|
||||
/>
|
||||
<Search />
|
||||
<div className="woocommerce-marketplace__header-meta">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { useContext, useEffect } from '@wordpress/element';
|
||||
import { Button } from '@wordpress/components';
|
||||
import classNames from 'classnames';
|
||||
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
|
||||
|
@ -12,10 +12,10 @@ import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
|
|||
*/
|
||||
import './tabs.scss';
|
||||
import { DEFAULT_TAB_KEY, MARKETPLACE_PATH } from '../constants';
|
||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||
import { MarketplaceContextType } from '../../contexts/types';
|
||||
|
||||
export interface TabsProps {
|
||||
selectedTab?: string | undefined;
|
||||
setSelectedTab: ( value: string ) => void;
|
||||
additionalClassNames?: Array< string > | undefined;
|
||||
}
|
||||
|
||||
|
@ -63,8 +63,8 @@ const setUrlTabParam = ( tabKey: string ) => {
|
|||
} );
|
||||
};
|
||||
|
||||
const renderTabs = ( props: TabsProps ) => {
|
||||
const { selectedTab, setSelectedTab } = props;
|
||||
const renderTabs = ( contextValue: MarketplaceContextType ) => {
|
||||
const { selectedTab, setSelectedTab } = contextValue;
|
||||
const tabContent = [];
|
||||
for ( const tabKey in tabs ) {
|
||||
tabContent.push(
|
||||
|
@ -104,14 +104,16 @@ const renderTabs = ( props: TabsProps ) => {
|
|||
};
|
||||
|
||||
const Tabs = ( props: TabsProps ): JSX.Element => {
|
||||
const { setSelectedTab, additionalClassNames } = props;
|
||||
const { additionalClassNames } = props;
|
||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||
const { setSelectedTab } = marketplaceContextValue;
|
||||
|
||||
interface Query {
|
||||
path?: string;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
const query: Query = useQuery();
|
||||
const query: Record<string, string> = useQuery();
|
||||
|
||||
useEffect( () => {
|
||||
if ( query?.tab && tabs[ query.tab ] ) {
|
||||
|
@ -128,7 +130,7 @@ const Tabs = ( props: TabsProps ): JSX.Element => {
|
|||
additionalClassNames || []
|
||||
) }
|
||||
>
|
||||
{ renderTabs( props ) }
|
||||
{ renderTabs( marketplaceContextValue ) }
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export type MarketplaceContextType = {
|
||||
isLoading: boolean;
|
||||
setIsLoading: ( isLoading: boolean ) => void;
|
||||
selectedTab: string;
|
||||
setSelectedTab: ( tab: string ) => void;
|
||||
};
|
|
@ -1,29 +1,22 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './marketplace.scss';
|
||||
import { DEFAULT_TAB_KEY } from './components/constants';
|
||||
import { MarketplaceContextProvider } from './contexts/marketplace-context';
|
||||
import Header from './components/header/header';
|
||||
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() {
|
||||
const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY );
|
||||
|
||||
return (
|
||||
<ProductListContextProvider>
|
||||
<MarketplaceContextProvider>
|
||||
<div className="woocommerce-marketplace">
|
||||
<Header
|
||||
selectedTab={ selectedTab }
|
||||
setSelectedTab={ setSelectedTab }
|
||||
/>
|
||||
<Content selectedTab={ selectedTab } />
|
||||
<Header />
|
||||
<Content />
|
||||
<FeedbackModal />
|
||||
<Footer />
|
||||
</div>
|
||||
</ProductListContextProvider>
|
||||
</MarketplaceContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
* Internal dependencies
|
||||
*/
|
||||
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 { LOCALE } from '../../utils/admin-settings';
|
||||
|
||||
|
@ -34,7 +34,7 @@ async function fetchDiscoverPageData(): Promise< ProductGroup[] > {
|
|||
}
|
||||
|
||||
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 ) {
|
||||
url = `${ url }?locale=${ LOCALE.userLocale }`;
|
||||
|
|
Loading…
Reference in New Issue