woocommerce/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx

439 lines
12 KiB
TypeScript

/**
* External dependencies
*/
import {
useContext,
useEffect,
useState,
useCallback,
} from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
import { speak } from '@wordpress/a11y';
import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './content.scss';
import { Product, ProductType } from '../product-list/types';
import { getAdminSetting } from '~/utils/admin-settings';
import Discover from '../discover/discover';
import Products from '../products/products';
import MySubscriptions from '../my-subscriptions/my-subscriptions';
import { MarketplaceContext } from '../../contexts/marketplace-context';
import { fetchSearchResults, getProductType } from '../../utils/functions';
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
import { SearchResultsCountType } from '../../contexts/types';
import {
recordMarketplaceView,
recordLegacyTabView,
} from '../../utils/tracking';
import InstallNewProductModal from '../install-flow/install-new-product-modal';
import Promotions from '../promotions/promotions';
import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice';
import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice';
import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subscriptions/subscriptions-expired-expiring-notice';
import LoadMoreButton from '../load-more-button/load-more-button';
export default function Content(): JSX.Element {
const marketplaceContextValue = useContext( MarketplaceContext );
const [ allProducts, setAllProducts ] = useState< Product[] >( [] );
const [ filteredProducts, setFilteredProducts ] = useState< Product[] >(
[]
);
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ totalPagesCategory, setTotalPagesCategory ] = useState( 1 );
const [ totalPagesExtensions, setTotalPagesExtensions ] = useState( 1 );
const [ totalPagesThemes, setTotalPagesThemes ] = useState( 1 );
const [ totalPagesBusinessServices, setTotalPagesBusinessServices ] =
useState( 1 );
const [ firstNewProductId, setFirstNewProductId ] = useState< number >( 0 );
const [ isLoadingMore, setIsLoadingMore ] = useState( false );
const {
isLoading,
setIsLoading,
selectedTab,
setHasBusinessServices,
setSearchResultsCount,
} = marketplaceContextValue;
const query = useQuery();
const searchCompleteAnnouncement = ( count: number ): void => {
speak(
sprintf(
// translators: %d is the number of products found.
__( '%d products found', 'woocommerce' ),
count
)
);
};
const tagProductsWithType = (
products: Product[],
type: ProductType
): Product[] => {
return products.map( ( product ) => ( {
...product,
type,
} ) );
};
const loadMoreProducts = useCallback( () => {
setIsLoadingMore( true );
const params = new URLSearchParams();
const abortController = new AbortController();
if ( query.category && query.category !== '_all' ) {
params.append( 'category', query.category );
}
if ( query.tab === 'themes' || query.tab === 'business-services' ) {
params.append( 'category', query.tab );
}
if ( query.term ) {
params.append( 'term', query.term );
}
const wccomSettings = getAdminSetting( 'wccomHelper', false );
if ( wccomSettings.storeCountry ) {
params.append( 'country', wccomSettings.storeCountry );
}
params.append( 'page', ( currentPage + 1 ).toString() );
fetchSearchResults( params, abortController.signal )
.then( ( productList ) => {
setAllProducts( ( prevProducts ) => {
const flattenedPrevProducts = Array.isArray(
prevProducts[ 0 ]
)
? prevProducts.flat()
: prevProducts;
const newProducts = productList.products.filter(
( newProduct ) =>
! flattenedPrevProducts.some(
( prevProduct ) =>
prevProduct.id === newProduct.id
)
);
if ( newProducts.length > 0 ) {
setFirstNewProductId( newProducts[ 0 ].id ?? 0 );
}
const combinedProducts = [
...flattenedPrevProducts,
...newProducts,
];
return combinedProducts;
} );
speak( __( 'More products loaded', 'woocommerce' ) );
setCurrentPage( ( prevPage ) => prevPage + 1 );
setIsLoadingMore( false );
} )
.catch( () => {
speak( __( 'Error loading more products', 'woocommerce' ) );
} )
.finally( () => {
setIsLoadingMore( false );
} );
return () => {
abortController.abort();
};
}, [
currentPage,
query.category,
query.term,
query.tab,
setIsLoadingMore,
] );
useEffect( () => {
// if it's a paginated request, don't use this effect
if ( currentPage > 1 ) {
return;
}
const categories: Array< {
category: keyof SearchResultsCountType;
type: ProductType;
} > = [
{ category: 'extensions', type: ProductType.extension },
{ category: 'themes', type: ProductType.theme },
{
category: 'business-services',
type: ProductType.businessService,
},
];
const abortControllers = categories.map( () => new AbortController() );
setIsLoading( true );
setAllProducts( [] );
// If query.category is present and not '_all', only fetch that category
if ( query.category && query.category !== '_all' ) {
const params = new URLSearchParams();
params.append( 'category', query.category );
if ( query.term ) {
params.append( 'term', query.term );
}
const wccomSettings = getAdminSetting( 'wccomHelper', false );
if ( wccomSettings.storeCountry ) {
params.append( 'country', wccomSettings.storeCountry );
}
fetchSearchResults( params, abortControllers[ 0 ].signal )
.then( ( productList ) => {
setAllProducts( productList.products );
setTotalPagesCategory( productList.totalPages );
setSearchResultsCount( {
[ query.tab ]: productList.totalProducts,
} );
searchCompleteAnnouncement( productList.totalProducts );
} )
.catch( () => {
setAllProducts( [] );
} )
.finally( () => {
setIsLoading( false );
} );
} else {
// Fetch all tabs when query.term or query.category changes
Promise.all(
categories.map( ( { category, type }, index ) => {
const params = new URLSearchParams();
if ( category !== 'extensions' ) {
params.append( 'category', category );
}
if ( query.term ) {
params.append( 'term', query.term );
}
const wccomSettings = getAdminSetting(
'wccomHelper',
false
);
if ( wccomSettings.storeCountry ) {
params.append( 'country', wccomSettings.storeCountry );
}
return fetchSearchResults(
params,
abortControllers[ index ].signal
).then( ( productList ) => {
const typedProducts = tagProductsWithType(
productList.products,
type
);
if ( category === 'business-services' ) {
setHasBusinessServices( typedProducts.length > 0 );
}
return {
products: typedProducts,
totalPages: productList.totalPages,
totalProducts: productList.totalProducts,
type,
};
} );
} )
)
.then( ( results ) => {
const combinedProducts = results.flatMap(
( result ) => result.products
);
setAllProducts( combinedProducts );
setSearchResultsCount( {
extensions: results.find(
( i ) => i.type === 'extension'
)?.totalProducts,
themes: results.find( ( i ) => i.type === 'theme' )
?.totalProducts,
'business-services': results.find(
( i ) => i.type === 'business-service'
)?.totalProducts,
} );
results.forEach( ( result ) => {
switch ( result.type ) {
case ProductType.extension:
setTotalPagesExtensions( result.totalPages );
break;
case ProductType.theme:
setTotalPagesThemes( result.totalPages );
break;
case ProductType.businessService:
setTotalPagesBusinessServices(
result.totalPages
);
break;
}
} );
searchCompleteAnnouncement(
results.reduce( ( acc, curr ) => {
return acc + curr.totalProducts;
}, 0 )
);
} )
.catch( () => {
setAllProducts( [] );
} )
.finally( () => {
setIsLoading( false );
} );
}
return () => {
abortControllers.forEach( ( controller ) => {
controller.abort();
} );
};
}, [
query.tab,
query.term,
query.category,
setHasBusinessServices,
setIsLoading,
setSearchResultsCount,
currentPage,
] );
// Filter the products based on the selected tab
useEffect( () => {
let filtered: Product[] | null;
switch ( selectedTab ) {
case 'extensions':
filtered = allProducts.filter(
( p ) => p.type === ProductType.extension
);
break;
case 'themes':
filtered = allProducts.filter(
( p ) => p.type === ProductType.theme
);
break;
case 'business-services':
filtered = allProducts.filter(
( p ) => p.type === ProductType.businessService
);
break;
default:
filtered = [];
}
setFilteredProducts( filtered );
}, [ selectedTab, allProducts ] );
// Record tab view events when the query changes
useEffect( () => {
const marketplaceViewProps = {
view: query?.tab,
search_term: query?.term,
product_type: query?.section,
category: query?.category,
};
recordMarketplaceView( marketplaceViewProps );
recordLegacyTabView( marketplaceViewProps );
}, [ query?.tab, query?.term, query?.section, query?.category ] );
// Reset current page when tab, term, or category changes
useEffect( () => {
setCurrentPage( 1 );
setFirstNewProductId( 0 );
}, [ selectedTab, query?.category, query?.term ] );
// Maintain product focus for accessibility
useEffect( () => {
if ( firstNewProductId ) {
setTimeout( () => {
const firstNewProduct = document.getElementById(
`product-${ firstNewProductId }`
);
if ( firstNewProduct ) {
firstNewProduct.focus();
}
}, 0 );
}
}, [ firstNewProductId ] );
const renderContent = (): JSX.Element => {
switch ( selectedTab ) {
case 'extensions':
case 'themes':
case 'business-services':
return (
<Products
products={ filteredProducts }
categorySelector={ true }
type={ getProductType( selectedTab ) }
/>
);
case 'discover':
return <Discover />;
case 'my-subscriptions':
return (
<SubscriptionsContextProvider>
<MySubscriptions />
</SubscriptionsContextProvider>
);
default:
return <></>;
}
};
const shouldShowLoadMoreButton = () => {
if ( ! query.category || query.category === '_all' ) {
// Check against total pages for the selected tab
switch ( selectedTab ) {
case 'extensions':
return currentPage < totalPagesExtensions;
case 'themes':
return currentPage < totalPagesThemes;
case 'business-services':
return currentPage < totalPagesBusinessServices;
default:
return false;
}
} else {
// Check against totalPagesCategory for specific category
return currentPage < totalPagesCategory;
}
};
return (
<div className="woocommerce-marketplace__content">
<Promotions />
<InstallNewProductModal products={ filteredProducts } />
{ selectedTab !== 'business-services' &&
selectedTab !== 'my-subscriptions' && <ConnectNotice /> }
{ selectedTab !== 'business-services' && <PluginInstallNotice /> }
{ selectedTab !== 'business-services' && (
<SubscriptionsExpiredExpiringNotice type="expired" />
) }
{ selectedTab !== 'business-services' && (
<SubscriptionsExpiredExpiringNotice type="expiring" />
) }
{ renderContent() }
{ ! isLoading && shouldShowLoadMoreButton() && (
<LoadMoreButton
onLoadMore={ loadMoreProducts }
isBusy={ isLoadingMore }
disabled={ isLoadingMore }
/>
) }
</div>
);
}