Commit 63c6107b authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge pull request #1543 from blockscout/category-tabs

New category tabs
parents 878cb938 3d8e3986
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
<g clip-path="url(#a)" fill="currentColor">
<path d="M5.176 5.291a1.253 1.253 0 0 0-1.76.159L.29 9.2a1.247 1.247 0 0 0 0 1.6l3.125 3.75a1.25 1.25 0 1 0 1.919-1.6L2.876 10l2.459-2.949a1.25 1.25 0 0 0-.159-1.76Z"/>
<path d="M5.176 5.291a1.253 1.253 0 0 0-1.76.159L.29 9.2a1.247 1.247 0 0 0 0 1.6l3.125 3.75a1.25 1.25 0 1 0 1.919-1.6L2.876 10l2.459-2.949a1.25 1.25 0 0 0-.159-1.76ZM11.494 2.525a1.261 1.261 0 0 0-1.47.981l-2.5 12.5A1.247 1.247 0 0 0 8.754 17.5a1.249 1.249 0 0 0 1.223-1.006l2.5-12.5a1.246 1.246 0 0 0-.982-1.469Z"/>
<path d="M11.494 2.525a1.261 1.261 0 0 0-1.47.981l-2.5 12.5A1.247 1.247 0 0 0 8.754 17.5a1.249 1.249 0 0 0 1.223-1.006l2.5-12.5a1.246 1.246 0 0 0-.982-1.469ZM19.708 9.2l-3.124-3.75a1.249 1.249 0 1 0-1.92 1.601l2.46 2.95-2.46 2.948a1.25 1.25 0 1 0 1.92 1.602l3.124-3.75a1.246 1.246 0 0 0 0-1.601Z"/>
<path d="M5.176 5.291a1.253 1.253 0 0 0-1.76.159L.29 9.2a1.247 1.247 0 0 0 0 1.6l3.125 3.75a1.25 1.25 0 1 0 1.919-1.6L2.876 10l2.459-2.949a1.25 1.25 0 0 0-.159-1.76Zm6.318-2.766a1.261 1.261 0 0 0-1.47.981l-2.5 12.5a1.247 1.247 0 0 0 1.23 1.494 1.249 1.249 0 0 0 1.223-1.006l2.5-12.5a1.246 1.246 0 0 0-.982-1.469Z"/>
<path d="M11.494 2.525a1.261 1.261 0 0 0-1.47.981l-2.5 12.5a1.247 1.247 0 0 0 1.23 1.494 1.249 1.249 0 0 0 1.223-1.006l2.5-12.5a1.246 1.246 0 0 0-.982-1.469ZM19.708 9.2l-3.124-3.75a1.249 1.249 0 1 0-1.92 1.601l2.46 2.95-2.46 2.948a1.25 1.25 0 1 0 1.92 1.602l3.124-3.75a1.246 1.246 0 0 0 0-1.601Z"/>
<path d="m19.708 9.2-3.124-3.75a1.249 1.249 0 1 0-1.92 1.601l2.46 2.95-2.46 2.948a1.25 1.25 0 1 0 1.92 1.602l3.124-3.75a1.246 1.246 0 0 0 0-1.601Z"/>
</g>
<defs>
......
......@@ -20,6 +20,6 @@ export type MarketplaceAppOverview = MarketplaceAppPreview & {
}
export enum MarketplaceCategory {
ALL = 'All apps',
ALL = 'All',
FAVORITES = 'Favorites',
}
......@@ -2,9 +2,12 @@ import { Grid } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import IconSvg from 'ui/shared/IconSvg';
import MarketplaceAppCard from './MarketplaceAppCard';
......@@ -15,9 +18,12 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
showDisclaimer: (id: string) => void;
selectedCategoryId?: string;
}
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading, showDisclaimer }: Props) => {
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading, showDisclaimer, selectedCategoryId }: Props) => {
const { value: isExperiment } = useFeatureValue('marketplace_exp', false);
return apps.length > 0 ? (
<Grid
templateColumns={{
......@@ -48,7 +54,18 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo
)) }
</Grid>
) : (
<EmptySearchResult text={ `Couldn${ apos }t find an app that matches your filter query.` }/>
<EmptySearchResult
text={
(selectedCategoryId === MarketplaceCategory.FAVORITES && !favoriteApps.length && isExperiment) ? (
<>
You don{ apos }t have any favorite apps.
Click on the <IconSvg name="star_outline" w={ 4 } h={ 4 } mb={ -0.5 }/> icon on the app{ apos }s card to add it to Favorites.
</>
) : (
`Couldn${ apos }t find an app that matches your filter query.`
)
}
/>
);
};
......
import _groudBy from 'lodash/groupBy';
import _pickBy from 'lodash/pickBy';
import _unique from 'lodash/uniq';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -74,7 +74,12 @@ export default function useMarketplace() {
const { isPlaceholderData, isError, error, data, displayedApps } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps);
const categories = React.useMemo(() => {
return _unique(data?.map(app => app.categories).flat()) || [];
const grouped = _groudBy(data, app => app.categories);
return Object.keys(grouped).map(category => ({
name: category,
count: grouped[category].length,
}));
// return _unique(data?.map(app => app.categories).flat()) || [];
}, [ data ]);
React.useEffect(() => {
......@@ -83,7 +88,7 @@ export default function useMarketplace() {
React.useEffect(() => {
if (!isPlaceholderData && !isError) {
const isValidDefaultCategory = categories.includes(defaultCategoryId);
const isValidDefaultCategory = categories.map(c => c.name).includes(defaultCategoryId);
isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId);
}
// run only when data is loaded
......@@ -128,6 +133,7 @@ export default function useMarketplace() {
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
appsTotal: data?.length || 0,
}), [
selectedCategoryId,
categories,
......@@ -145,5 +151,6 @@ export default function useMarketplace() {
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
data?.length,
]);
}
import { Box } from '@chakra-ui/react';
import React from 'react';
import { MarketplaceCategory } from 'types/client/marketplace';
import type { TabItem } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList';
import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import useMarketplace from '../marketplace/useMarketplace';
const feature = config.features.marketplace;
......@@ -31,8 +38,45 @@ const Marketplace = () => {
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
appsTotal,
} = useMarketplace();
const { value: isExperiment } = useFeatureValue('marketplace_exp', false);
const categoryTabs = React.useMemo(() => {
const tabs: Array<TabItem> = categories.map(category => ({
id: category.name,
title: category.name,
count: category.count,
component: null,
}));
tabs.unshift({
id: MarketplaceCategory.ALL,
title: MarketplaceCategory.ALL,
count: appsTotal,
component: null,
});
tabs.unshift({
id: MarketplaceCategory.FAVORITES,
title: () => <IconSvg name="star_outline" w={ 4 } h={ 4 }/>,
count: null,
component: null,
});
return tabs;
}, [ categories, appsTotal ]);
const selectedCategoryIndex = React.useMemo(() => {
const index = categoryTabs.findIndex(c => c.id === selectedCategoryId);
return index === -1 ? 0 : index;
}, [ categoryTabs, selectedCategoryId ]);
const handleCategoryChange = React.useCallback((index: number) => {
onCategoryChange(categoryTabs[index].id);
}, [ categoryTabs, onCategoryChange ]);
throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null });
if (!feature.isEnabled) {
......@@ -43,16 +87,32 @@ const Marketplace = () => {
return (
<>
{ isExperiment && (
<Box marginTop={{ base: 0, lg: 8 }}>
{ isPlaceholderData ? (
<TabsSkeleton tabs={ categoryTabs }/>
) : (
<TabsWithScroll
tabs={ categoryTabs }
onTabChange={ handleCategoryChange }
defaultTabIndex={ selectedCategoryIndex }
marginBottom={{ base: 0, lg: -2 }}
/>
) }
</Box>
) }
<Box
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
>
<MarketplaceCategoriesMenu
categories={ categories }
selectedCategoryId={ selectedCategoryId }
onSelect={ onCategoryChange }
isLoading={ isPlaceholderData }
/>
{ !isExperiment && (
<MarketplaceCategoriesMenu
categories={ categories.map(c => c.name) }
selectedCategoryId={ selectedCategoryId }
onSelect={ onCategoryChange }
isLoading={ isPlaceholderData }
/>
) }
<FilterInput
initialValue={ filterQuery }
......@@ -71,6 +131,7 @@ const Marketplace = () => {
onFavoriteClick={ onFavoriteClick }
isLoading={ isPlaceholderData }
showDisclaimer={ showDisclaimer }
selectedCategoryId={ selectedCategoryId }
/>
{ (selectedApp && isAppInfoModalOpen) && (
......
......@@ -4,7 +4,7 @@ import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
text: string;
text: string | JSX.Element;
}
const EmptySearchResult = ({ text }: Props) => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment