Commit b352e18e authored by Max Alekseenko's avatar Max Alekseenko

new category tabs

parent 5e61c159
...@@ -20,6 +20,6 @@ export type MarketplaceAppOverview = MarketplaceAppPreview & { ...@@ -20,6 +20,6 @@ export type MarketplaceAppOverview = MarketplaceAppPreview & {
} }
export enum MarketplaceCategory { export enum MarketplaceCategory {
ALL = 'All apps', ALL = 'All',
FAVORITES = 'Favorites', FAVORITES = 'Favorites',
} }
...@@ -2,9 +2,12 @@ import { Grid } from '@chakra-ui/react'; ...@@ -2,9 +2,12 @@ import { Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; 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 { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/shared/EmptySearchResult'; import EmptySearchResult from 'ui/shared/EmptySearchResult';
import IconSvg from 'ui/shared/IconSvg';
import MarketplaceAppCard from './MarketplaceAppCard'; import MarketplaceAppCard from './MarketplaceAppCard';
...@@ -15,9 +18,12 @@ type Props = { ...@@ -15,9 +18,12 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean; isLoading: boolean;
showDisclaimer: (id: string) => void; 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 ? ( return apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
...@@ -48,7 +54,18 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo ...@@ -48,7 +54,18 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo
)) } )) }
</Grid> </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 _pickBy from 'lodash/pickBy';
import _unique from 'lodash/uniq';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -74,7 +74,12 @@ export default function useMarketplace() { ...@@ -74,7 +74,12 @@ export default function useMarketplace() {
const { isPlaceholderData, isError, error, data, displayedApps } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps); const { isPlaceholderData, isError, error, data, displayedApps } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps);
const categories = React.useMemo(() => { 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 ]); }, [ data ]);
React.useEffect(() => { React.useEffect(() => {
...@@ -83,7 +88,7 @@ export default function useMarketplace() { ...@@ -83,7 +88,7 @@ export default function useMarketplace() {
React.useEffect(() => { React.useEffect(() => {
if (!isPlaceholderData && !isError) { if (!isPlaceholderData && !isError) {
const isValidDefaultCategory = categories.includes(defaultCategoryId); const isValidDefaultCategory = categories.map(c => c.name).includes(defaultCategoryId);
isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId); isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId);
} }
// run only when data is loaded // run only when data is loaded
...@@ -128,6 +133,7 @@ export default function useMarketplace() { ...@@ -128,6 +133,7 @@ export default function useMarketplace() {
isAppInfoModalOpen, isAppInfoModalOpen,
isDisclaimerModalOpen, isDisclaimerModalOpen,
showDisclaimer, showDisclaimer,
appsTotal: data?.length || 0,
}), [ }), [
selectedCategoryId, selectedCategoryId,
categories, categories,
...@@ -145,5 +151,6 @@ export default function useMarketplace() { ...@@ -145,5 +151,6 @@ export default function useMarketplace() {
isAppInfoModalOpen, isAppInfoModalOpen,
isDisclaimerModalOpen, isDisclaimerModalOpen,
showDisclaimer, showDisclaimer,
data?.length,
]); ]);
} }
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from '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 config from 'configs/app';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'; import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import FilterInput from 'ui/shared/filters/FilterInput'; 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'; import useMarketplace from '../marketplace/useMarketplace';
const feature = config.features.marketplace; const feature = config.features.marketplace;
...@@ -30,8 +37,45 @@ const Marketplace = () => { ...@@ -30,8 +37,45 @@ const Marketplace = () => {
isAppInfoModalOpen, isAppInfoModalOpen,
isDisclaimerModalOpen, isDisclaimerModalOpen,
showDisclaimer, showDisclaimer,
appsTotal,
} = useMarketplace(); } = 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 ]);
if (isError) { if (isError) {
throw new Error('Unable to get apps list', { cause: error }); throw new Error('Unable to get apps list', { cause: error });
} }
...@@ -44,16 +88,32 @@ const Marketplace = () => { ...@@ -44,16 +88,32 @@ const Marketplace = () => {
return ( 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 <Box
display="flex" display="flex"
flexDirection={{ base: 'column', sm: 'row' }} flexDirection={{ base: 'column', sm: 'row' }}
> >
<MarketplaceCategoriesMenu { !isExperiment && (
categories={ categories } <MarketplaceCategoriesMenu
selectedCategoryId={ selectedCategoryId } categories={ categories.map(c => c.name) }
onSelect={ onCategoryChange } selectedCategoryId={ selectedCategoryId }
isLoading={ isPlaceholderData } onSelect={ onCategoryChange }
/> isLoading={ isPlaceholderData }
/>
) }
<FilterInput <FilterInput
initialValue={ filterQuery } initialValue={ filterQuery }
...@@ -72,6 +132,7 @@ const Marketplace = () => { ...@@ -72,6 +132,7 @@ const Marketplace = () => {
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
showDisclaimer={ showDisclaimer } showDisclaimer={ showDisclaimer }
selectedCategoryId={ selectedCategoryId }
/> />
{ (selectedApp && isAppInfoModalOpen) && ( { (selectedApp && isAppInfoModalOpen) && (
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
text: string; text: string | JSX.Element;
} }
const EmptySearchResult = ({ text }: Props) => { 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