Commit e43c48b0 authored by isstuev's avatar isstuev

add apps to search results

parent b38b5a8e
import { useQuery } from '@tanstack/react-query';
import _pickBy from 'lodash/pickBy'; import _pickBy from 'lodash/pickBy';
import _unique from 'lodash/uniq'; import _unique from 'lodash/uniq';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import useApiFetch from 'lib/hooks/useFetch';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { MARKETPLACE_APP } from 'stubs/marketplace';
import useMarketplaceApps from './useMarketplaceApps';
const favoriteAppsLocalStorageKey = 'favoriteApps'; const favoriteAppsLocalStorageKey = 'favoriteApps';
...@@ -24,16 +20,6 @@ function getFavoriteApps() { ...@@ -24,16 +20,6 @@ function getFavoriteApps() {
} }
} }
function isAppNameMatches(q: string, app: MarketplaceAppOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, favoriteApps: Array<string>) {
return category === MarketplaceCategory.ALL ||
(category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) ||
app.categories.includes(category);
}
export default function useMarketplace() { export default function useMarketplace() {
const router = useRouter(); const router = useRouter();
const defaultCategoryId = getQueryParamString(router.query.category); const defaultCategoryId = getQueryParamString(router.query.category);
...@@ -44,16 +30,6 @@ export default function useMarketplace() { ...@@ -44,16 +30,6 @@ export default function useMarketplace() {
const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery); const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery);
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]); const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const apiFetch = useApiFetch();
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch(appConfig.marketplace.configUrl || ''),
{
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: Array(9).fill(MARKETPLACE_APP),
staleTime: Infinity,
});
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => { const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps(); const favoriteApps = getFavoriteApps();
...@@ -79,9 +55,7 @@ export default function useMarketplace() { ...@@ -79,9 +55,7 @@ export default function useMarketplace() {
setSelectedCategoryId(newCategory); setSelectedCategoryId(newCategory);
}, []); }, []);
const displayedApps = React.useMemo(() => { const { isPlaceholderData, isError, error, data, displayedApps } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps);
return data?.filter(app => isAppNameMatches(debouncedFilterQuery, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
}, [ selectedCategoryId, data, debouncedFilterQuery, favoriteApps ]);
const categories = React.useMemo(() => { const categories = React.useMemo(() => {
return _unique(data?.map(app => app.categories).flat()) || []; return _unique(data?.map(app => app.categories).flat()) || [];
......
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch';
import { MARKETPLACE_APP } from 'stubs/marketplace';
function isAppNameMatches(q: string, app: MarketplaceAppOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, favoriteApps: Array<string>) {
return category === MarketplaceCategory.ALL ||
(category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) ||
app.categories.includes(category);
}
export default function useMarketplaceApps(filter: string, selectedCategoryId: string = MarketplaceCategory.ALL, favoriteApps: Array<string> = []) {
const apiFetch = useApiFetch();
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch(appConfig.marketplace.configUrl || ''),
{
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: Array(9).fill(MARKETPLACE_APP),
staleTime: Infinity,
});
const displayedApps = React.useMemo(() => {
return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
}, [ selectedCategoryId, data, filter, favoriteApps ]);
return React.useMemo(() => ({
data,
displayedApps,
error,
isError,
isPlaceholderData,
}), [
data,
displayedApps,
error,
isError,
isPlaceholderData,
]);
}
...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import React from 'react'; import React from 'react';
import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps';
import SearchResultListItem from 'ui/searchResults/SearchResultListItem'; import SearchResultListItem from 'ui/searchResults/SearchResultListItem';
import SearchResultsInput from 'ui/searchResults/SearchResultsInput'; import SearchResultsInput from 'ui/searchResults/SearchResultsInput';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem'; import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
...@@ -22,6 +23,8 @@ const SearchResultsPageContent = () => { ...@@ -22,6 +23,8 @@ const SearchResultsPageContent = () => {
const { data, isError, isPlaceholderData, pagination } = query; const { data, isError, isPlaceholderData, pagination } = query;
const [ showContent, setShowContent ] = React.useState(false); const [ showContent, setShowContent ] = React.useState(false);
const marketplaceApps = useMarketplaceApps(debouncedSearchTerm);
React.useEffect(() => { React.useEffect(() => {
if (showContent) { if (showContent) {
return; return;
...@@ -61,14 +64,23 @@ const SearchResultsPageContent = () => { ...@@ -61,14 +64,23 @@ const SearchResultsPageContent = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (!data?.items.length) { const hasData = data?.items.length || (pagination.page === 1 && marketplaceApps.displayedApps.length);
if (!hasData) {
return null; return null;
} }
return ( return (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map((item, index) => ( { marketplaceApps.displayedApps.map((item, index) => (
<SearchResultListItem
key={ 'actual_' + index }
data={{ type: 'app', app: item }}
searchTerm={ debouncedSearchTerm }
/>
)) }
{ data && data.items.map((item, index) => (
<SearchResultListItem <SearchResultListItem
key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index } key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index }
data={ item } data={ item }
...@@ -88,7 +100,14 @@ const SearchResultsPageContent = () => { ...@@ -88,7 +100,14 @@ const SearchResultsPageContent = () => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.items.map((item, index) => ( { marketplaceApps.displayedApps.map((item, index) => (
<SearchResultTableItem
key={ 'actual_' + index }
data={{ type: 'app', app: item }}
searchTerm={ debouncedSearchTerm }
/>
)) }
{ data && data.items.map((item, index) => (
<SearchResultTableItem <SearchResultTableItem
key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index } key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index }
data={ item } data={ item }
...@@ -108,6 +127,8 @@ const SearchResultsPageContent = () => { ...@@ -108,6 +127,8 @@ const SearchResultsPageContent = () => {
return null; return null;
} }
const resultsCount = pagination.page === 1 && !data?.next_page_params ? (data?.items.length || 0) + marketplaceApps.displayedApps.length : '50+';
const text = isPlaceholderData && pagination.page === 1 ? ( const text = isPlaceholderData && pagination.page === 1 ? (
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ pagination.isVisible ? 0 : 6 }/> <Skeleton h={ 6 } w="280px" borderRadius="full" mb={ pagination.isVisible ? 0 : 6 }/>
) : ( ) : (
...@@ -115,7 +136,7 @@ const SearchResultsPageContent = () => { ...@@ -115,7 +136,7 @@ const SearchResultsPageContent = () => {
<Box mb={ pagination.isVisible ? 0 : 6 } lineHeight="32px"> <Box mb={ pagination.isVisible ? 0 : 6 } lineHeight="32px">
<span>Found </span> <span>Found </span>
<chakra.span fontWeight={ 700 }> <chakra.span fontWeight={ 700 }>
{ pagination.page > 1 ? 50 : data?.items.length }{ data?.next_page_params || pagination.page > 1 ? '+' : '' } { resultsCount }
</chakra.span> </chakra.span>
<span> matching result{ (data?.items && data.items.length > 1) || pagination.page > 1 ? 's' : '' } for </span> <span> matching result{ (data?.items && data.items.length > 1) || pagination.page > 1 ? 's' : '' } for </span>
<chakra.span fontWeight={ 700 }>{ debouncedSearchTerm }</chakra.span> <chakra.span fontWeight={ 700 }>{ debouncedSearchTerm }</chakra.span>
......
import { Flex, Grid, Icon, Box, Text, chakra, Skeleton } from '@chakra-ui/react'; import { Flex, Grid, Icon, Image, Box, Text, chakra, Skeleton, useColorMode } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -16,13 +16,15 @@ import Address from 'ui/shared/address/Address'; ...@@ -16,13 +16,15 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils'; import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: SearchResultItem; data: SearchResultItem | SearchResultAppItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean; isLoading?: boolean;
} }
...@@ -38,6 +40,8 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -38,6 +40,8 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
}); });
}, [ searchTerm ]); }, [ searchTerm ]);
const { colorMode } = useColorMode();
const firstRow = (() => { const firstRow = (() => {
switch (data.type) { switch (data.type) {
case 'token': { case 'token': {
...@@ -84,7 +88,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -84,7 +88,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
case 'label': { case 'label': {
return ( return (
<Flex alignItems="flex-start"> <Flex alignItems="center">
<Icon as={ labelIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/> <Icon as={ labelIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<LinkInternal <LinkInternal
href={ route({ pathname: '/address/[hash]', query: { hash: data.address } }) } href={ route({ pathname: '/address/[hash]', query: { hash: data.address } }) }
...@@ -99,6 +103,42 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -99,6 +103,42 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
); );
} }
case 'app': {
const title = <span dangerouslySetInnerHTML={{ __html: highlightText(data.app.title, searchTerm) }}/>;
return (
<Flex alignItems="center">
<Image
borderRadius="base"
boxSize={ 6 }
mr={ 2 }
src={ colorMode === 'dark' && data.app.logoDarkMode ? data.app.logoDarkMode : data.app.logo }
alt={ `${ data.app.title } app icon` }
/>
{ data.app.external ? (
<LinkExternal
href={ data.app.url }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
onClick={ handleLinkClick }
>
{ title }
</LinkExternal>
) : (
<LinkInternal
href={ route({ pathname: '/apps/[id]', query: { id: data.app.id } }) }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
onClick={ handleLinkClick }
>
{ title }
</LinkInternal>
) }
</Flex>
);
}
case 'block': { case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase(); const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return ( return (
...@@ -138,7 +178,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -138,7 +178,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
return ( return (
<Grid templateColumns={ templateCols } alignItems="center" gap={ 2 }> <Grid templateColumns={ templateCols } alignItems="center" gap={ 2 }>
<Skeleton isLoaded={ !isLoading } overflow="hidden" display="flex" alignItems="center"> <Skeleton isLoaded={ !isLoading } overflow="hidden" display="flex" alignItems="center">
<Text variant="secondary" whiteSpace="nowrap" overflow="hidden"> <Text whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.address } isTooltipDisabled/> <HashStringShortenDynamic hash={ data.address } isTooltipDisabled/>
</Text> </Text>
{ data.is_smart_contract_verified && <Icon as={ iconSuccess } color="green.500" ml={ 1 }/> } { data.is_smart_contract_verified && <Icon as={ iconSuccess } color="green.500" ml={ 1 }/> }
...@@ -176,6 +216,21 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -176,6 +216,21 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
</Flex> </Flex>
); );
} }
case 'app': {
return (
<Text
overflow="hidden"
textOverflow="ellipsis"
sx={{
display: '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': '3',
}}
>
{ data.app.description }
</Text>
);
}
case 'contract': case 'contract':
case 'address': { case 'address': {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase(); const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
...@@ -198,7 +253,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -198,7 +253,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
</Skeleton> </Skeleton>
</Flex> </Flex>
{ Boolean(secondRow) && ( { Boolean(secondRow) && (
<Box w="100%" overflow="hidden" whiteSpace="nowrap"> <Box w="100%" overflow="hidden">
{ secondRow } { secondRow }
</Box> </Box>
) } ) }
......
import { chakra, Tr, Td, Text, Flex, Icon, Box, Skeleton } from '@chakra-ui/react'; import { chakra, Tr, Td, Text, Flex, Icon, Image, Box, Skeleton, useColorMode } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -16,12 +16,14 @@ import Address from 'ui/shared/address/Address'; ...@@ -16,12 +16,14 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils'; import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: SearchResultItem; data: SearchResultItem | SearchResultAppItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean; isLoading?: boolean;
} }
...@@ -37,6 +39,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -37,6 +39,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
}); });
}, [ searchTerm ]); }, [ searchTerm ]);
const { colorMode } = useColorMode();
const content = (() => { const content = (() => {
switch (data.type) { switch (data.type) {
case 'token': { case 'token': {
...@@ -160,6 +164,55 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -160,6 +164,55 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
); );
} }
case 'app': {
const title = <span dangerouslySetInnerHTML={{ __html: highlightText(data.app.title, searchTerm) }}/>;
return (
<>
<Td fontSize="sm">
<Flex alignItems="center">
<Image
borderRadius="base"
boxSize={ 6 }
mr={ 2 }
src={ colorMode === 'dark' && data.app.logoDarkMode ? data.app.logoDarkMode : data.app.logo }
alt={ `${ data.app.title } app icon` }
/>
{ data.app.external ? (
<LinkExternal
href={ data.app.url }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
onClick={ handleLinkClick }
>
{ title }
</LinkExternal>
) : (
<LinkInternal
href={ route({ pathname: '/apps/[id]', query: { id: data.app.id } }) }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
onClick={ handleLinkClick }
>
{ title }
</LinkInternal>
) }
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle" colSpan={ 2 }>
<Text
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ data.app.description }
</Text>
</Td>
</>
);
}
case 'block': { case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase(); const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
......
import type { SearchResultItem } from 'types/api/search'; import type { SearchResultItem } from 'types/api/search';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
export type Category = 'token' | 'nft' | 'address' | 'app' | 'public_tag' | 'transaction' | 'block'; export type ApiCategory = 'token' | 'nft' | 'address' | 'public_tag' | 'transaction' | 'block';
export type Category = ApiCategory | 'app';
export type ItemsCategoriesMap =
Record<ApiCategory, Array<SearchResultItem>> &
Record<'app', Array<MarketplaceAppOverview>>;
export type SearchResultAppItem = {
type: 'app';
app: MarketplaceAppOverview;
}
export const searchCategories: Array<{id: Category; title: string }> = [ export const searchCategories: Array<{id: Category; title: string }> = [
{ id: 'app', title: 'Apps' },
{ id: 'token', title: 'Tokens (ERC-20)' }, { id: 'token', title: 'Tokens (ERC-20)' },
{ id: 'nft', title: 'NFTs (ERC-721 & 1155)' }, { id: 'nft', title: 'NFTs (ERC-721 & 1155)' },
{ id: 'address', title: 'Addresses' }, { id: 'address', title: 'Addresses' },
{ id: 'app', title: 'Apps' },
{ id: 'public_tag', title: 'Public tags' }, { id: 'public_tag', title: 'Public tags' },
{ id: 'transaction', title: 'Transactions' }, { id: 'transaction', title: 'Transactions' },
{ id: 'block', title: 'Blocks' }, { id: 'block', title: 'Blocks' },
]; ];
export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleShort: string }> = { export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleShort: string }> = {
app: { itemTitle: 'App', itemTitleShort: 'App' },
token: { itemTitle: 'Token', itemTitleShort: 'Token' }, token: { itemTitle: 'Token', itemTitleShort: 'Token' },
nft: { itemTitle: 'NFT', itemTitleShort: 'NFT' }, nft: { itemTitle: 'NFT', itemTitleShort: 'NFT' },
address: { itemTitle: 'Address', itemTitleShort: 'Address' }, address: { itemTitle: 'Address', itemTitleShort: 'Address' },
app: { itemTitle: 'App', itemTitleShort: 'App' },
public_tag: { itemTitle: 'Public tag', itemTitleShort: 'Tag' }, public_tag: { itemTitle: 'Public tag', itemTitleShort: 'Tag' },
transaction: { itemTitle: 'Transaction', itemTitleShort: 'Txn' }, transaction: { itemTitle: 'Transaction', itemTitleShort: 'Txn' },
block: { itemTitle: 'Block', itemTitleShort: 'Block' }, block: { itemTitle: 'Block', itemTitleShort: 'Block' },
}; };
export function getItemCategory(item: SearchResultItem): Category | undefined { export function getItemCategory(item: SearchResultItem | SearchResultAppItem): Category | undefined {
switch (item.type) { switch (item.type) {
case 'address': case 'address':
case 'contract': { case 'contract': {
...@@ -43,5 +54,8 @@ export function getItemCategory(item: SearchResultItem): Category | undefined { ...@@ -43,5 +54,8 @@ export function getItemCategory(item: SearchResultItem): Category | undefined {
case 'transaction': { case 'transaction': {
return 'transaction'; return 'transaction';
} }
case 'app': {
return 'app';
}
} }
} }
...@@ -33,7 +33,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -33,7 +33,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const recentSearchKeywords = getRecentSearchKeywords(); const recentSearchKeywords = getRecentSearchKeywords();
const { searchTerm, handleSearchTermChange, query, pathname } = useSearchQuery(); const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query, pathname } = useSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -142,7 +142,12 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -142,7 +142,12 @@ const SearchBar = ({ isHomepage }: Props) => {
<SearchBarRecentKeywords onClick={ handleSearchTermChange } onClear={ onClose }/> <SearchBarRecentKeywords onClick={ handleSearchTermChange } onClear={ onClose }/>
) } ) }
{ searchTerm.trim().length > 0 && ( { searchTerm.trim().length > 0 && (
<SearchBarSuggest query={ query } searchTerm={ searchTerm } onItemClick={ handleItemClick } containerId={ SCROLL_CONTAINER_ID }/> <SearchBarSuggest
query={ query }
searchTerm={ debouncedSearchTerm }
onItemClick={ handleItemClick }
containerId={ SCROLL_CONTAINER_ID }
/>
) } ) }
</Box> </Box>
</PopoverBody> </PopoverBody>
......
...@@ -3,15 +3,15 @@ import throttle from 'lodash/throttle'; ...@@ -3,15 +3,15 @@ import throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import type { SearchResultItem } from 'types/api/search';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import type { Category } from 'ui/shared/search/utils'; import type { ItemsCategoriesMap } from 'ui/shared/search/utils';
import { getItemCategory, searchCategories } from 'ui/shared/search/utils'; import { getItemCategory, searchCategories } from 'ui/shared/search/utils';
import SearchBarSuggestApp from './SearchBarSuggestApp';
import SearchBarSuggestItem from './SearchBarSuggestItem'; import SearchBarSuggestItem from './SearchBarSuggestItem';
interface Props { interface Props {
...@@ -24,6 +24,8 @@ interface Props { ...@@ -24,6 +24,8 @@ interface Props {
const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props) => { const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const marketplaceApps = useMarketplaceApps(searchTerm);
const categoriesRefs = React.useRef<Array<HTMLParagraphElement>>([]); const categoriesRefs = React.useRef<Array<HTMLParagraphElement>>([]);
const tabsRef = React.useRef<HTMLDivElement>(null); const tabsRef = React.useRef<HTMLDivElement>(null);
...@@ -61,10 +63,10 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -61,10 +63,10 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
}, [ containerId, handleScroll ]); }, [ containerId, handleScroll ]);
const itemsGroups = React.useMemo(() => { const itemsGroups = React.useMemo(() => {
if (!query.data?.items) { if (!query.data?.items && !marketplaceApps.displayedApps) {
return {}; return {};
} }
const map: Partial<Record<Category, Array<SearchResultItem>>> = {}; const map: Partial<ItemsCategoriesMap> = {};
query.data?.items.forEach(item => { query.data?.items.forEach(item => {
const cat = getItemCategory(item); const cat = getItemCategory(item);
if (cat) { if (cat) {
...@@ -75,8 +77,11 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -75,8 +77,11 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
} }
} }
}); });
if (marketplaceApps.displayedApps.length) {
map.app = marketplaceApps.displayedApps;
}
return map; return map;
}, [ query.data?.items ]); }, [ query.data?.items, marketplaceApps.displayedApps ]);
const scrollToCategory = React.useCallback((index: number) => () => { const scrollToCategory = React.useCallback((index: number) => () => {
setTabIndex(index); setTabIndex(index);
...@@ -91,7 +96,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -91,7 +96,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
const bgColor = useColorModeValue('white', 'gray.900'); const bgColor = useColorModeValue('white', 'gray.900');
const content = (() => { const content = (() => {
if (query.isLoading) { if (query.isLoading || marketplaceApps.isPlaceholderData) {
return <ContentLoader text="We are searching, please wait... " fontSize="sm"/>; return <ContentLoader text="We are searching, please wait... " fontSize="sm"/>;
} }
...@@ -129,9 +134,12 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -129,9 +134,12 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
> >
{ cat.title } { cat.title }
</Text> </Text>
{ itemsGroups[cat.id]?.map((item, index) => { cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) =>
<SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>, <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>,
) } ) }
{ cat.id === 'app' && itemsGroups[cat.id]?.map((item, index) =>
<SearchBarSuggestApp key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>,
) }
</Element> </Element>
); );
}) } }) }
......
import { Link, Icon, Image, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import arrowIcon from 'icons/arrows/north-east.svg';
import highlightText from 'lib/highlightText';
import SearchBarSuggestItemLink from './SearchBarSuggestItemLink';
interface Props {
data: MarketplaceAppOverview;
isMobile: boolean | undefined;
searchTerm: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
}
const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => {
const logo = (
<Image
borderRadius="base"
boxSize={ 6 }
src={ useColorModeValue(data.logo, data.logoDarkMode || data.logo) }
alt={ `${ data.title } app icon` }
/>
);
const content = (() => {
if (isMobile) {
return (
<>
<Flex alignItems="center">
{ logo }
<Text
fontWeight={ 700 }
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
ml={ 2 }
>
<span dangerouslySetInnerHTML={{ __html: highlightText(data.title, searchTerm) }}/>
</Text>
{ data.external && <Icon as={ arrowIcon } boxSize={ 4 } verticalAlign="middle"/> }
</Flex>
<Text
variant="secondary"
overflow="hidden"
textOverflow="ellipsis"
sx={{
display: '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': '3',
}}
>
{ data.description }
</Text>
</>
);
}
return (
<Flex gap={ 2 } alignItems="center">
{ logo }
<Text
fontWeight={ 700 }
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
w="200px"
flexShrink={ 0 }
>
<span dangerouslySetInnerHTML={{ __html: highlightText(data.title, searchTerm) }}/>
</Text>
<Text
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
flexGrow={ 1 }
>
{ data.description }
</Text>
{ data.external && <Icon as={ arrowIcon } boxSize={ 4 } verticalAlign="middle"/> }
</Flex>
);
})();
if (data.external) {
return (
<Link href={ data.url } target="_blank" cursor="auto" _hover={{ textDecoration: 'none' }}>
<SearchBarSuggestItemLink onClick={ onClick }>
{ content }
</SearchBarSuggestItemLink>
</Link>
);
}
return (
<NextLink href={{ pathname: '/apps/[id]', query: { id: data.id } }} passHref legacyBehavior>
<SearchBarSuggestItemLink onClick={ onClick }>
{ content }
</SearchBarSuggestItemLink>
</NextLink>
);
};
export default React.memo(SearchBarSuggestItem);
import { chakra, useColorModeValue } from '@chakra-ui/react';
import type { LinkProps as NextLinkProps } from 'next/link'; import type { LinkProps as NextLinkProps } from 'next/link';
import NextLink from 'next/link'; import NextLink from 'next/link';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -8,6 +7,7 @@ import type { SearchResultItem } from 'types/api/search'; ...@@ -8,6 +7,7 @@ import type { SearchResultItem } from 'types/api/search';
import SearchBarSuggestAddress from './SearchBarSuggestAddress'; import SearchBarSuggestAddress from './SearchBarSuggestAddress';
import SearchBarSuggestBlock from './SearchBarSuggestBlock'; import SearchBarSuggestBlock from './SearchBarSuggestBlock';
import SearchBarSuggestItemLink from './SearchBarSuggestItemLink';
import SearchBarSuggestLabel from './SearchBarSuggestLabel'; import SearchBarSuggestLabel from './SearchBarSuggestLabel';
import SearchBarSuggestToken from './SearchBarSuggestToken'; import SearchBarSuggestToken from './SearchBarSuggestToken';
import SearchBarSuggestTx from './SearchBarSuggestTx'; import SearchBarSuggestTx from './SearchBarSuggestTx';
...@@ -64,28 +64,9 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => ...@@ -64,28 +64,9 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) =>
return ( return (
<NextLink href={ url as NextLinkProps['href'] } passHref legacyBehavior> <NextLink href={ url as NextLinkProps['href'] } passHref legacyBehavior>
<chakra.a <SearchBarSuggestItemLink onClick={ onClick }>
py={ 3 }
px={ 1 }
display="flex"
flexDir="column"
rowGap={ 2 }
borderColor="divider"
borderBottomWidth="1px"
_last={{
borderBottomWidth: '0',
}}
_hover={{
bgColor: useColorModeValue('blue.50', 'gray.800'),
}}
fontSize="sm"
_first={{
mt: 2,
}}
onClick={ onClick }
>
{ content } { content }
</chakra.a> </SearchBarSuggestItemLink>
</NextLink> </NextLink>
); );
}; };
......
import { chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
type Props = {
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
children: React.ReactNode;
}
const SearchBarSuggestItemLink = ({ onClick, children }: Props) => {
return (
<chakra.a
py={ 3 }
px={ 1 }
display="flex"
flexDir="column"
rowGap={ 2 }
borderColor="divider"
borderBottomWidth="1px"
_last={{
borderBottomWidth: '0',
}}
_hover={{
bgColor: useColorModeValue('blue.50', 'gray.800'),
}}
fontSize="sm"
_first={{
mt: 2,
}}
onClick={ onClick }
>
{ children }
</chakra.a>
);
};
export default SearchBarSuggestItemLink;
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