Commit af982cd9 authored by Yuri Mikhin's avatar Yuri Mikhin Committed by Yuri Mikhin

Add marketplace categories.

parent 1b704fec
export enum MarketplaceCategoryNames { export enum MarketplaceCategoryId {
'all',
'favorites',
'defi', 'defi',
'exchanges', 'exchanges',
'finance', 'finance',
...@@ -11,12 +13,16 @@ export enum MarketplaceCategoryNames { ...@@ -11,12 +13,16 @@ export enum MarketplaceCategoryNames {
'yieldFarming', 'yieldFarming',
} }
export type MarketplaceCategoriesIds = keyof typeof MarketplaceCategoryId;
export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string }
export type AppItemPreview = { export type AppItemPreview = {
id: string; id: string;
title: string; title: string;
logo: string; logo: string;
shortDescription: string; shortDescription: string;
categories: Array<keyof typeof MarketplaceCategoryNames>; categories: Array<MarketplaceCategoriesIds>;
} }
export type AppItemOverview = AppItemPreview & { export type AppItemOverview = AppItemPreview & {
......
import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react'; import { Grid, GridItem, Heading, VisuallyHidden } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react'; import React from 'react';
import type { AppItemPreview } from 'types/client/apps'; import type { AppItemPreview } from 'types/client/apps';
...@@ -14,37 +14,11 @@ type Props = { ...@@ -14,37 +14,11 @@ type Props = {
onAppClick: (id: string) => void; onAppClick: (id: string) => void;
displayedAppId: string | null; displayedAppId: string | null;
onModalClose: () => void; onModalClose: () => void;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
} }
function getFavoriteApps() { const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps, onFavoriteClick }: Props) => {
try {
return JSON.parse(localStorage.getItem('favoriteApps') || '[]');
} catch (e) {
return [];
}
}
const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
const result = favoriteApps.filter((appId: string) => appId !== id);
setFavoriteApps(result);
localStorage.setItem('favoriteApps', JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem('favoriteApps', JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
return ( return (
<> <>
<VisuallyHidden> <VisuallyHidden>
...@@ -72,7 +46,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => { ...@@ -72,7 +46,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
shortDescription={ app.shortDescription } shortDescription={ app.shortDescription }
categories={ app.categories } categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) } isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ handleFavoriteClick } onFavoriteClick={ onFavoriteClick }
/> />
</GridItem> </GridItem>
)) } )) }
...@@ -86,7 +60,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => { ...@@ -86,7 +60,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
id={ displayedAppId } id={ displayedAppId }
onClose={ onModalClose } onClose={ onModalClose }
isFavorite={ favoriteApps.includes(displayedAppId) } isFavorite={ favoriteApps.includes(displayedAppId) }
onFavoriteClick={ handleFavoriteClick } onFavoriteClick={ onFavoriteClick }
/> />
) } ) }
</> </>
......
...@@ -5,7 +5,7 @@ import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton'; ...@@ -5,7 +5,7 @@ import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
const applicationStubs = [ ...Array(12) ]; const applicationStubs = [ ...Array(12) ];
export const AppListSkeleton = () => { const AppListSkeleton = () => {
return ( return (
<Grid <Grid
templateColumns={{ templateColumns={{
...@@ -25,3 +25,5 @@ export const AppListSkeleton = () => { ...@@ -25,3 +25,5 @@ export const AppListSkeleton = () => {
</Grid> </Grid>
); );
}; };
export default AppListSkeleton;
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
import NextLink from 'next/link'; import NextLink from 'next/link';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoryNames } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json'; import marketplaceApps from 'data/marketplaceApps.json';
import linkIcon from 'icons/link.svg'; import linkIcon from 'icons/link.svg';
...@@ -161,7 +161,7 @@ const AppModal = ({ ...@@ -161,7 +161,7 @@ const AppModal = ({
</Heading> </Heading>
<Box marginBottom={ 2 }> <Box marginBottom={ 2 }>
{ categories.map((category: keyof typeof MarketplaceCategoryNames) => APP_CATEGORIES[category] && ( { categories.map((category: MarketplaceCategoriesIds) => APP_CATEGORIES[category] && (
<Tag <Tag
colorScheme="blue" colorScheme="blue"
marginRight={ 2 } marginRight={ 2 }
......
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem';
import { APP_CATEGORIES } from './constants';
const categoriesList = Object.keys(APP_CATEGORIES).map((id: string) => ({
id: id,
name: APP_CATEGORIES[id as MarketplaceCategoriesIds],
})) as Array<MarketplaceCategory>;
type Props = {
selectedCategoryId: MarketplaceCategoriesIds;
onSelect: (category: MarketplaceCategoriesIds) => void;
}
const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId);
return (
<Menu>
<MenuButton
as={ Button }
mb={{ base: 2, sm: 0 }}
mr={{ base: 0, sm: 2 }}
size="md"
variant="outline"
colorScheme="gray"
flexShrink={ 0 }
>
<Box
as="span"
display="flex"
alignItems="center"
>
{ selectedCategory ? selectedCategory.name : 'All' }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
<MenuList zIndex={ 3 }>
{ categoriesList.map((category: MarketplaceCategory) => (
<CategoriesMenuItem
key={ category.id }
id={ category.id }
name={ category.name }
onClick={ onSelect }
/>
)) }
</MenuList>
</Menu>
);
};
export default React.memo(CategoriesMenu);
import { MenuItem } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { MarketplaceCategoriesIds } from 'types/client/apps';
type Props = {
id: MarketplaceCategoriesIds;
name: string;
onClick: (category: MarketplaceCategoriesIds) => void;
}
const CategoriesMenuItem = ({ id, name, onClick }: Props) => {
const handleSelection = useCallback(() => {
onClick(id);
}, [ id, onClick ]);
return (
<MenuItem
key={ id }
onClick={ handleSelection }
>
{ name }
</MenuItem>
);
};
export default CategoriesMenuItem;
import type { MarketplaceCategoryNames } from 'types/client/apps'; import type { MarketplaceCategoriesIds } from 'types/client/apps';
export const APP_CATEGORIES: {[key in keyof typeof MarketplaceCategoryNames]: string} = { export const APP_CATEGORIES: {[key in MarketplaceCategoriesIds]: string} = {
all: 'All',
favorites: 'Favorites',
defi: 'DeFi', defi: 'DeFi',
exchanges: 'Exchanges', exchanges: 'Exchanges',
finance: 'Finance', finance: 'Finance',
......
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import useNetwork from 'lib/hooks/useNetwork';
const favoriteAppsLocalStorageKey = 'favoriteApps';
function getFavoriteApps() {
try {
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]');
} catch (e) {
return [];
}
}
function isAppNameMatches(q: string, app: AppItemOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOverview, favoriteApps: Array<string>) {
return category === 'all' ||
(category === 'favorites' && favoriteApps.includes(app.id)) ||
app.categories.includes(category);
}
export default function useMarketplaceApps() {
const selectedNetwork = useNetwork();
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const [ category, setCategory ] = useState<MarketplaceCategoriesIds>('all');
const [ filterQuery, setFilterQuery ] = useState('');
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
const result = favoriteApps.filter((appId: string) => appId !== id);
setFavoriteApps(result);
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => setFilterQuery(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
const filterApps = useCallback((q: string, category: MarketplaceCategoriesIds) => {
const apps = defaultAppList
?.filter(app => {
return isAppNameMatches(q, app) && isAppCategoryMatches(category, app, favoriteApps);
});
setDisplayedApps(apps || []);
}, [ defaultAppList, favoriteApps ]);
const handleCategoryChange = useCallback((newCategory: MarketplaceCategoriesIds) => {
setCategory(newCategory);
}, []);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
useEffect(() => {
filterApps(filterQuery, category);
}, [ filterQuery, category, filterApps ]);
useEffect(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainId === selectedNetwork?.chainId)
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
return React.useMemo(() => ({
category,
handleCategoryChange,
debounceFilterApps,
isLoading,
displayedApps,
showAppInfo,
displayedAppId,
clearDisplayedAppId,
favoriteApps,
handleFavoriteClick,
}), [ category,
clearDisplayedAppId,
debounceFilterApps,
displayedAppId, displayedApps,
favoriteApps,
handleCategoryChange,
handleFavoriteClick,
isLoading,
showAppInfo,
]);
}
import { Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link } from '@chakra-ui/react';
import debounce from 'lodash/debounce'; import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
import useNetwork from 'lib/hooks/useNetwork';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/FilterInput';
import { AppListSkeleton } from '../apps/AppListSkeleton'; import useMarketplaceApps from '../apps/useMarkeplaceApps';
const Apps = () => { const Apps = () => {
const selectedNetwork = useNetwork(); const {
const [ isLoading, setIsLoading ] = useState(true); isLoading,
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>(); category,
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]); handleCategoryChange,
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null); debounceFilterApps,
showAppInfo,
const showAppInfo = useCallback((id: string) => { displayedApps,
setDisplayedAppId(id); displayedAppId,
}, []); clearDisplayedAppId,
// eslint-disable-next-line react-hooks/exhaustive-deps favoriteApps,
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), [ defaultAppList ]); handleFavoriteClick,
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []); } = useMarketplaceApps();
function filterApps(q: string) {
const apps = defaultAppList
?.filter(app => app.title.toLowerCase().includes(q.toLowerCase()));
setDisplayedApps(apps || []);
}
useEffect(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainId === selectedNetwork?.chainId)
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
return ( return (
<> <>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/> <Box
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
>
<CategoriesMenu
selectedCategoryId={ category }
onSelect={ handleCategoryChange }
/>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
</Box>
{ isLoading ? <AppListSkeleton/> : ( { isLoading ? <AppListSkeleton/> : (
<AppList <AppList
...@@ -57,6 +43,8 @@ const Apps = () => { ...@@ -57,6 +43,8 @@ const Apps = () => {
onAppClick={ showAppInfo } onAppClick={ showAppInfo }
displayedAppId={ displayedAppId } displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId } onModalClose={ clearDisplayedAppId }
favoriteApps={ favoriteApps }
onFavoriteClick={ handleFavoriteClick }
/> />
) } ) }
......
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