Marketplace: Implement search and category filtering (#39608)

This commit is contained in:
Cem Ünalan 2023-08-15 10:12:41 +03:00 committed by GitHub
commit a1f26ada0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 405 additions and 146 deletions

View File

@ -4,15 +4,39 @@
import { Dropdown } from '@wordpress/components'; import { Dropdown } from '@wordpress/components';
import { chevronDown, chevronUp, Icon } from '@wordpress/icons'; import { chevronDown, chevronUp, Icon } from '@wordpress/icons';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { navigateTo, getNewPath } from '@woocommerce/navigation';
import classNames from 'classnames';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { Category } from './category-selector'; import { Category } from './types';
function DropdownContent( props: { function DropdownContent( props: {
readonly categories: Category[]; readonly categories: Category[];
readonly selected?: Category;
readonly onClick: () => void;
} ): JSX.Element { } ): JSX.Element {
function updateCategorySelection(
event: React.MouseEvent< HTMLButtonElement >
) {
const slug = event.currentTarget.value;
if ( ! slug ) {
return;
}
/**
* Trigger the onClick event on the parent component to close the dropdown.
* This closes the dropdown automatically when a user clicks on an item.
*/
props.onClick();
navigateTo( {
url: getNewPath( { category: slug } ),
} );
}
return ( return (
<ul className="woocommerce-marketplace__category-dropdown-list"> <ul className="woocommerce-marketplace__category-dropdown-list">
{ props.categories.map( ( category ) => ( { props.categories.map( ( category ) => (
@ -20,7 +44,17 @@ function DropdownContent( props: {
className="woocommerce-marketplace__category-dropdown-item" className="woocommerce-marketplace__category-dropdown-item"
key={ category.slug } key={ category.slug }
> >
<button className="woocommerce-marketplace__category-dropdown-item-button"> <button
className={ classNames(
'woocommerce-marketplace__category-dropdown-item-button',
{
'woocommerce-marketplace__category-dropdown-item-button--selected':
category.slug === props.selected?.slug,
}
) }
value={ category.slug }
onClick={ updateCategorySelection }
>
{ category.label } { category.label }
</button> </button>
</li> </li>
@ -36,6 +70,7 @@ type CategoryDropdownProps = {
buttonClassName?: string; buttonClassName?: string;
contentClassName?: string; contentClassName?: string;
arrowIconSize?: number; arrowIconSize?: number;
selected?: Category;
}; };
export default function CategoryDropdown( export default function CategoryDropdown(
@ -60,8 +95,12 @@ export default function CategoryDropdown(
</button> </button>
) } ) }
className={ props.className } className={ props.className }
renderContent={ () => ( renderContent={ ( { onToggle } ) => (
<DropdownContent categories={ props.categories } /> <DropdownContent
categories={ props.categories }
selected={ props.selected }
onClick={ onToggle }
/>
) } ) }
contentClassName={ props.contentClassName } contentClassName={ props.contentClassName }
/> />

View File

@ -2,13 +2,28 @@
* External dependencies * External dependencies
*/ */
import classNames from 'classnames'; import classNames from 'classnames';
import { navigateTo, getNewPath } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { Category } from './category-selector'; import { Category } from './types';
export default function CategoryLink( props: Category ): JSX.Element { export default function CategoryLink( props: Category ): JSX.Element {
function updateCategorySelection(
event: React.MouseEvent< HTMLButtonElement >
) {
const slug = event.currentTarget.value;
if ( ! slug ) {
return;
}
navigateTo( {
url: getNewPath( { category: slug } ),
} );
}
const classes = classNames( const classes = classNames(
'woocommerce-marketplace__category-item-button', 'woocommerce-marketplace__category-item-button',
{ {
@ -17,5 +32,13 @@ export default function CategoryLink( props: Category ): JSX.Element {
} }
); );
return <button className={ classes }>{ props.label }</button>; return (
<button
className={ classes }
onClick={ updateCategorySelection }
value={ props.slug }
>
{ props.label }
</button>
);
} }

View File

@ -3,6 +3,7 @@
.woocommerce-marketplace__category-selector { .woocommerce-marketplace__category-selector {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
margin: $grid-unit-20 0 0 0;
} }
.woocommerce-marketplace__category-item { .woocommerce-marketplace__category-item {
@ -29,6 +30,7 @@
&--selected { &--selected {
color: $white; color: $white;
background-color: $gray-900; background-color: $gray-900;
fill: $white;
} }
} }
@ -106,4 +108,19 @@
padding: 6px $grid-unit-10; padding: 6px $grid-unit-10;
line-height: 20px; line-height: 20px;
width: 100%; width: 100%;
&--selected {
color: $white;
background-color: $gray-900;
}
}
.woocommerce-marketplace__category-selector-loading {
display: flex;
margin-top: $grid-unit-20;
p {
margin: 0;
line-height: $grid-unit-30;
}
} }

View File

@ -4,50 +4,50 @@
import { useState, useEffect } from '@wordpress/element'; import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Spinner } from '@wordpress/components'; import { Spinner } from '@wordpress/components';
import { useQuery } from '@woocommerce/navigation';
import classNames from 'classnames';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import CategoryLink from './category-link'; import CategoryLink from './category-link';
import './category-selector.scss';
import CategoryDropdown from './category-dropdown'; import CategoryDropdown from './category-dropdown';
import { MARKETPLACE_URL } from '../constants'; import { Category, CategoryAPIItem } from './types';
import { fetchCategories } from '../../utils/functions';
import './category-selector.scss';
export type Category = { const ALL_CATEGORIES_SLUG = '_all';
readonly slug: string;
readonly label: string;
selected: boolean;
};
export type CategoryAPIItem = {
readonly slug: string;
readonly label: string;
};
function fetchCategories(): Promise< CategoryAPIItem[] > {
return fetch( MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/categories' )
.then( ( response ) => {
if ( ! response.ok ) {
throw new Error( response.statusText );
}
return response.json();
} )
.then( ( json ) => {
return json;
} )
.catch( () => {
return [];
} );
}
export default function CategorySelector(): JSX.Element { export default function CategorySelector(): JSX.Element {
const [ firstBatch, setFirstBatch ] = useState< Category[] >( [] ); const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] );
const [ secondBatch, setSecondBatch ] = useState< Category[] >( [] ); const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] );
const [ selected, setSelected ] = useState< Category >();
const [ isLoading, setIsLoading ] = useState( false ); const [ isLoading, setIsLoading ] = useState( false );
const query = useQuery();
useEffect( () => {
// If no category is selected, show All as selected
let categoryToSearch = ALL_CATEGORIES_SLUG;
if ( query.category ) {
categoryToSearch = query.category;
}
const allCategories = visibleItems.concat( dropdownItems );
const selectedCategory = allCategories.find(
( category ) => category.slug === categoryToSearch
);
if ( selectedCategory ) {
setSelected( selectedCategory );
}
}, [ query, visibleItems, dropdownItems ] );
useEffect( () => { useEffect( () => {
setIsLoading( true ); setIsLoading( true );
fetchCategories() fetchCategories()
.then( ( categoriesFromAPI: CategoryAPIItem[] ) => { .then( ( categoriesFromAPI: CategoryAPIItem[] ) => {
const categories: Category[] = categoriesFromAPI.map( const categories: Category[] = categoriesFromAPI.map(
@ -61,7 +61,7 @@ export default function CategorySelector(): JSX.Element {
// Put the "All" category to the beginning // Put the "All" category to the beginning
categories.sort( ( a ) => { categories.sort( ( a ) => {
if ( a.slug === '_all' ) { if ( a.slug === ALL_CATEGORIES_SLUG ) {
return -1; return -1;
} }
@ -69,55 +69,94 @@ export default function CategorySelector(): JSX.Element {
} ); } );
// Split array into two from 7th item // Split array into two from 7th item
const firstBatchCategories = categories.slice( 0, 7 ); const visibleCategoryItems = categories.slice( 0, 7 );
const secondBatchCategories = categories.slice( 7 ); const dropdownCategoryItems = categories.slice( 7 );
setFirstBatch( firstBatchCategories ); setVisibleItems( visibleCategoryItems );
setSecondBatch( secondBatchCategories ); setDropdownItems( dropdownCategoryItems );
} )
.catch( () => {
setVisibleItems( [] );
setDropdownItems( [] );
} ) } )
.finally( () => { .finally( () => {
setIsLoading( false ); setIsLoading( false );
} ); } );
}, [] ); }, [] );
function mobileCategoryDropdownLabel() {
const allCategoriesText = __( 'All Categories', 'woocommerce' );
if ( ! selected ) {
return allCategoriesText;
}
if ( selected.label === 'All' ) {
return allCategoriesText;
}
return selected.label;
}
function isSelectedInDropdown() {
if ( ! selected ) {
return false;
}
return dropdownItems.find(
( category ) => category.slug === selected.slug
);
}
if ( isLoading ) { if ( isLoading ) {
return ( return (
<> <div className="woocommerce-marketplace__category-selector-loading">
{ __( 'Loading categories…', 'woocommerce' ) } <p>{ __( 'Loading categories…', 'woocommerce' ) }</p>
<Spinner /> <Spinner />
</> </div>
); );
} }
return ( return (
<> <>
<ul className="woocommerce-marketplace__category-selector"> <ul className="woocommerce-marketplace__category-selector">
{ firstBatch.map( ( category ) => ( { visibleItems.map( ( category ) => (
<li <li
className="woocommerce-marketplace__category-item" className="woocommerce-marketplace__category-item"
key={ category.slug } key={ category.slug }
> >
<CategoryLink { ...category } /> <CategoryLink
{ ...category }
selected={ category.slug === selected?.slug }
/>
</li> </li>
) ) } ) ) }
<li className="woocommerce-marketplace__category-item"> <li className="woocommerce-marketplace__category-item">
<CategoryDropdown <CategoryDropdown
label={ __( 'More', 'woocommerce' ) } label={ __( 'More', 'woocommerce' ) }
categories={ secondBatch } categories={ dropdownItems }
buttonClassName="woocommerce-marketplace__category-item-button" buttonClassName={ classNames(
'woocommerce-marketplace__category-item-button',
{
'woocommerce-marketplace__category-item-button--selected':
isSelectedInDropdown(),
}
) }
contentClassName="woocommerce-marketplace__category-item-content" contentClassName="woocommerce-marketplace__category-item-content"
arrowIconSize={ 20 } arrowIconSize={ 20 }
selected={ selected }
/> />
</li> </li>
</ul> </ul>
<div className="woocommerce-marketplace__category-selector--full-width"> <div className="woocommerce-marketplace__category-selector--full-width">
<CategoryDropdown <CategoryDropdown
label={ __( 'All Categories', 'woocommerce' ) } label={ mobileCategoryDropdownLabel() }
categories={ firstBatch.concat( secondBatch ) } categories={ visibleItems.concat( dropdownItems ) }
buttonClassName="woocommerce-marketplace__category-dropdown-button" buttonClassName="woocommerce-marketplace__category-dropdown-button"
className="woocommerce-marketplace__category-dropdown" className="woocommerce-marketplace__category-dropdown"
contentClassName="woocommerce-marketplace__category-dropdown-content" contentClassName="woocommerce-marketplace__category-dropdown-content"
selected={ selected }
/> />
</div> </div>
</> </>

View File

@ -0,0 +1,10 @@
export type Category = {
readonly slug: string;
readonly label: string;
selected: boolean;
};
export type CategoryAPIItem = {
readonly slug: string;
readonly label: string;
};

View File

@ -6,14 +6,5 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 40px; gap: 40px;
padding: 0 $content-spacing-small;
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace {
&__discover {
padding: 0 $content-spacing-large;
}
} }
} }

View File

@ -1,23 +1,45 @@
/** /**
* External dependencies * External dependencies
*/ */
import { useContext } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
/** /**
* Internal dependencies * Internal dependencies
*/ */
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 ProductListContent from '../product-list-content/product-list-content';
import ProductLoader from '../product-loader/product-loader';
export default function Extensions(): JSX.Element { export default function Extensions(): JSX.Element {
const productListContextValue = useContext( ProductListContext );
const { productList, isLoading } = productListContextValue;
const products = productList.slice( 0, 60 );
let title = __( 'Extensions', 'woocommerce' );
if ( products.length > 0 ) {
title = sprintf(
// translators: %s: number of extensions
__( '%s extensions', 'woocommerce' ),
products.length
);
}
return ( return (
<div className="woocommerce-marketplace__extensions"> <div className="woocommerce-marketplace__extensions">
<h2 className="woocommerce-marketplace__product-list-title woocommerce-marketplace__product-list-title--extensions">
{ title }
</h2>
<CategorySelector /> <CategorySelector />
<div className="woocommerce-marketplace__product-list-content"> { isLoading ? (
<div className="woocommerce-marketplace__extension-card"></div> <ProductLoader />
<div className="woocommerce-marketplace__extension-card"></div> ) : (
<div className="woocommerce-marketplace__extension-card"></div> <ProductListContent products={ products } />
<div className="woocommerce-marketplace__extension-card"></div> ) }
</div>
</div> </div>
); );
} }

View File

@ -8,7 +8,7 @@ import { Button, Card } from '@wordpress/components';
* Internal dependencies * Internal dependencies
*/ */
import './product-card.scss'; import './product-card.scss';
import { Product } from '../product-list-content/product-list-content'; import { Product } from '../product-list/types';
import { getAdminSetting } from '../../../../client/utils/admin-settings'; import { getAdminSetting } from '../../../../client/utils/admin-settings';
export interface ProductCardProps { export interface ProductCardProps {

View File

@ -9,6 +9,7 @@
&__extension-card { &__extension-card {
background-color: #3c3c3c; background-color: #3c3c3c;
color: $white;
height: 270px; height: 270px;
} }
} }

View File

@ -2,30 +2,12 @@
* Internal dependencies * Internal dependencies
*/ */
import './product-list-content.scss'; import './product-list-content.scss';
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @woocommerce/dependency-group
import ProductCard from '../product-card/product-card'; import ProductCard from '../product-card/product-card';
import { Product } from '../product-list/types';
export interface Product { export default function ProductListContent( props: {
id?: number;
title: string;
description: string;
vendorName: string;
vendorUrl: string;
icon: string;
url: string;
price: string | number;
productType?: string;
averageRating?: number | null;
reviewsCount?: number | null;
currency?: string;
}
interface ProductListContentProps {
products: Product[]; products: Product[];
} } ): JSX.Element {
export default function ProductListContent(
props: ProductListContentProps
): JSX.Element {
const { products } = props; const { products } = props;
return ( return (
<div className="woocommerce-marketplace__product-list-content"> <div className="woocommerce-marketplace__product-list-content">

View File

@ -13,6 +13,11 @@
font-size: 20px; font-size: 20px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
margin-top: $small-gap;
}
&__product-list-title--extensions {
margin-bottom: 0;
} }
&__product-list-link { &__product-list-link {

View File

@ -1,14 +1,9 @@
/**
* External dependencies
*/
/** /**
* Internal dependencies * Internal dependencies
*/ */
import ProductListContent, { import ProductListContent from '../product-list-content/product-list-content';
Product,
} from '../product-list-content/product-list-content';
import ProductListHeader from '../product-list-header/product-list-header'; import ProductListHeader from '../product-list-header/product-list-header';
import { Product } from './types';
interface ProductListProps { interface ProductListProps {
title: string; title: string;

View File

@ -0,0 +1,31 @@
export type SearchAPIProductType = {
title: string;
image: string;
excerpt: string;
link: string;
demo_url: string;
price: string;
hash: string;
slug: string;
id: number;
rating: number | null;
reviews_count: number | null;
vendor_name: string;
vendor_url: string;
icon: string;
};
export interface Product {
id?: number;
title: string;
description: string;
vendorName: string;
vendorUrl: string;
icon: string;
url: string;
price: string | number;
productType?: string;
averageRating?: number | null;
reviewsCount?: number | null;
currency?: string;
}

View File

@ -1,6 +1,10 @@
@import '../../stylesheets/_variables.scss'; @import '../../stylesheets/_variables.scss';
.woocommerce-marketplace { .woocommerce-marketplace {
&__product-loader {
margin-top: $grid-unit-20;
}
&__product-loader-cards { &__product-loader-cards {
display: grid; display: grid;
background: linear-gradient(to right, $gray-0 40%, $gray-5 60%, $gray-0 80%); background: linear-gradient(to right, $gray-0 40%, $gray-5 60%, $gray-0 80%);

View File

@ -3,60 +3,40 @@
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Icon, search } from '@wordpress/icons'; import { Icon, search } from '@wordpress/icons';
import { useState } from '@wordpress/element'; import { useEffect, useState } from '@wordpress/element';
import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './search.scss'; import './search.scss';
import { MARKETPLACE_URL } from '../constants';
const searchPlaceholder = __( 'Search extensions and themes', 'woocommerce' ); const searchPlaceholder = __( 'Search extensions and themes', 'woocommerce' );
const marketplaceAPI = MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/search';
export interface SearchProps {
locale?: string | 'en_US';
country?: string | undefined;
}
/** /**
* Search component. * Search component.
* *
* @param {SearchProps} props - Search props: locale and country.
* @return {JSX.Element} Search component. * @return {JSX.Element} Search component.
*/ */
function Search( props: SearchProps ): JSX.Element { function Search(): JSX.Element {
const locale = props.locale ?? 'en_US';
const country = props.country ?? '';
const [ searchTerm, setSearchTerm ] = useState( '' ); const [ searchTerm, setSearchTerm ] = useState( '' );
const build_parameter_string = ( const query = useQuery();
query_string: string,
query_country: string, useEffect( () => {
query_locale: string if ( query.term ) {
): string => { setSearchTerm( query.term );
const params = new URLSearchParams(); }
params.append( 'term', query_string ); }, [ query.term ] );
params.append( 'country', query_country );
params.append( 'locale', query_locale );
return params.toString();
};
const runSearch = () => { const runSearch = () => {
const query = searchTerm.trim(); const term = searchTerm.trim();
if ( ! query ) {
return [];
}
const params = build_parameter_string( query, country, locale ); // When the search term changes, we reset the category on purpose.
fetch( marketplaceAPI + '?' + params, { navigateTo( {
method: 'GET', url: getNewPath( { term, category: null, tab: 'extensions' } ),
} )
.then( ( response ) => response.json() )
.then( ( response ) => {
return response;
} ); } );
return []; return [];
}; };
@ -70,6 +50,7 @@ function Search( props: SearchProps ): JSX.Element {
if ( event.key === 'Enter' ) { if ( event.key === 'Enter' ) {
runSearch(); runSearch();
} }
if ( event.key === 'Escape' ) { if ( event.key === 'Escape' ) {
setSearchTerm( '' ); setSearchTerm( '' );
} }

View File

@ -34,9 +34,3 @@
border-bottom: 1px solid $gutenberg-gray-300; border-bottom: 1px solid $gutenberg-gray-300;
} }
} }
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace__tabs {
padding: 0 $content-spacing-large;
}
}

View File

@ -0,0 +1,104 @@
/**
* External dependencies
*/
import { useQuery } from '@woocommerce/navigation';
import { useState, useEffect, createContext } from '@wordpress/element';
/**
* Internal dependencies
*/
import {
Product,
SearchAPIProductType,
} from '../components/product-list/types';
import { MARKETPLACE_URL } from '../components/constants';
type ProductListContextType = {
productList: Product[];
isLoading: boolean;
};
export const ProductListContext = createContext< ProductListContextType >( {
productList: [],
isLoading: false,
} );
type ProductListContextProviderProps = {
children: JSX.Element;
country?: string;
locale?: string;
};
export function ProductListContextProvider(
props: ProductListContextProviderProps
): JSX.Element {
const [ isLoading, setIsLoading ] = useState( false );
const [ productList, setProductList ] = useState< Product[] >( [] );
const contextValue = {
productList,
isLoading,
};
const query = useQuery();
useEffect( () => {
setIsLoading( true );
const params = new URLSearchParams();
params.append( 'term', query.term ?? '' );
params.append( 'country', props.country ?? '' );
params.append( 'locale', props.locale ?? '' );
if ( query.category ) {
params.append( 'category', query.category );
}
const wccomSearchEndpoint =
MARKETPLACE_URL +
'/wp-json/wccom-extensions/1.0/search?' +
params.toString();
// Fetch data from WCCOM API
fetch( wccomSearchEndpoint )
.then( ( response ) => response.json() )
.then( ( response ) => {
/**
* Product card component expects a Product type.
* So we build that object from the API response.
*/
const products = response.products.map(
( product: SearchAPIProductType ): Product => {
return {
id: product.id,
title: product.title,
description: product.excerpt,
vendorName: product.vendor_name,
vendorUrl: product.vendor_url,
icon: product.icon,
url: product.link,
price: product.price,
averageRating: product.rating ?? 0,
reviewsCount: product.reviews_count ?? 0,
currency: '',
};
}
);
setProductList( products );
} )
.catch( () => {
setProductList( [] );
} )
.finally( () => {
setIsLoading( false );
} );
}, [ query, props.country, props.locale ] );
return (
<ProductListContext.Provider value={ contextValue }>
{ props.children }
</ProductListContext.Provider>
);
}

View File

@ -10,11 +10,13 @@ import './marketplace.scss';
import { DEFAULT_TAB_KEY } from './components/constants'; import { DEFAULT_TAB_KEY } from './components/constants';
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';
export default function Marketplace() { export default function Marketplace() {
const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY ); const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY );
return ( return (
<ProductListContextProvider>
<div className="woocommerce-marketplace"> <div className="woocommerce-marketplace">
<Header <Header
selectedTab={ selectedTab } selectedTab={ selectedTab }
@ -22,5 +24,6 @@ export default function Marketplace() {
/> />
<Content selectedTab={ selectedTab } /> <Content selectedTab={ selectedTab } />
</div> </div>
</ProductListContextProvider>
); );
} }

View File

@ -1,8 +1,9 @@
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { Product } from '../components/product-list-content/product-list-content'; import { Product } from '../components/product-list/types';
import { MARKETPLACE_URL } from '../components/constants'; import { MARKETPLACE_URL } from '../components/constants';
import { CategoryAPIItem } from '../components/category-selector/types';
interface ProductGroup { interface ProductGroup {
id: number; id: number;
@ -30,4 +31,21 @@ const fetchDiscoverPageData = async (): Promise< Array< ProductGroup > > => {
} ); } );
}; };
export { fetchDiscoverPageData, ProductGroup }; function fetchCategories(): Promise< CategoryAPIItem[] > {
return fetch( MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/categories' )
.then( ( response ) => {
if ( ! response.ok ) {
throw new Error( response.statusText );
}
return response.json();
} )
.then( ( json ) => {
return json;
} )
.catch( () => {
return [];
} );
}
export { fetchDiscoverPageData, fetchCategories, ProductGroup };