[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 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';
|
||||||
|
|
|
@ -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 />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
||||||
|
|
|
@ -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' );
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
@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 {
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__footer-content {
|
||||||
|
box-sizing: content-box;
|
||||||
|
max-width: $content-max-width;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__footer-title {
|
||||||
color: $gutenberg-gray-900;
|
color: $gutenberg-gray-900;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
@ -23,17 +28,13 @@
|
||||||
margin: 0 0 $content-spacing-large;
|
margin: 0 0 $content-spacing-large;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.woocommerce-marketplace__footer-columns {
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-columns {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $large-gap;
|
gap: $large-gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-logo {
|
.woocommerce-marketplace__footer-logo {
|
||||||
color: $wp-gray-50;
|
color: $wp-gray-50;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -42,7 +43,6 @@
|
||||||
gap: $small-gap;
|
gap: $small-gap;
|
||||||
margin: 48px 0 0;
|
margin: 48px 0 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: $breakpoint-medium) {
|
@media screen and (min-width: $breakpoint-medium) {
|
||||||
.woocommerce-marketplace__footer {
|
.woocommerce-marketplace__footer {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
* 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }`;
|
||||||
|
|
Loading…
Reference in New Issue