Commit c8d6980b authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Support metadata_tag type in search (#2559)

* Support metadata_tag type in search

* Update ui/searchResults/SearchResultListItem.tsx
Co-authored-by: default avatartom goriunov <tom@ohhhh.me>

---------
Co-authored-by: default avatartom goriunov <tom@ohhhh.me>
parent bcd5c2b6
...@@ -8,6 +8,7 @@ import type { ...@@ -8,6 +8,7 @@ import type {
SearchResultUserOp, SearchResultUserOp,
SearchResultBlob, SearchResultBlob,
SearchResultDomain, SearchResultDomain,
SearchResultMetadataTag,
} from 'types/api/search'; } from 'types/api/search';
export const token1: SearchResultToken = { export const token1: SearchResultToken = {
...@@ -147,6 +148,42 @@ export const domain1: SearchResultDomain = { ...@@ -147,6 +148,42 @@ export const domain1: SearchResultDomain = {
url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
}; };
export const metatag1: SearchResultMetadataTag = {
...address1,
type: 'metadata_tag',
metadata: {
name: 'utko',
slug: 'utko',
meta: {},
tagType: 'name',
ordinal: 1,
},
};
export const metatag2: SearchResultMetadataTag = {
...address2,
type: 'metadata_tag',
metadata: {
name: 'utko',
slug: 'utko',
meta: {},
tagType: 'name',
ordinal: 1,
},
};
export const metatag3: SearchResultMetadataTag = {
...contract2,
type: 'metadata_tag',
metadata: {
name: 'super utko',
slug: 'super-utko',
meta: {},
tagType: 'protocol',
ordinal: 1,
},
};
export const baseResponse: SearchResult = { export const baseResponse: SearchResult = {
items: [ items: [
token1, token1,
...@@ -157,6 +194,8 @@ export const baseResponse: SearchResult = { ...@@ -157,6 +194,8 @@ export const baseResponse: SearchResult = {
tx1, tx1,
blob1, blob1,
domain1, domain1,
metatag1,
], ],
next_page_params: null, next_page_params: null,
}; };
import type * as bens from '@blockscout/bens-types'; import type * as bens from '@blockscout/bens-types';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
export type SearchResultType = 'token' | 'address' | 'block' | 'transaction' | 'contract'; import type { AddressMetadataTagApi } from './addressMetadata';
export const SEARCH_RESULT_TYPES = {
token: 'token',
address: 'address',
block: 'block',
transaction: 'transaction',
contract: 'contract',
ens_domain: 'ens_domain',
label: 'label',
user_operation: 'user_operation',
blob: 'blob',
metadata_tag: 'metadata_tag',
} as const;
export type SearchResultType = typeof SEARCH_RESULT_TYPES[keyof typeof SEARCH_RESULT_TYPES];
export interface SearchResultToken { export interface SearchResultToken {
type: 'token'; type: 'token';
...@@ -20,29 +35,35 @@ export interface SearchResultToken { ...@@ -20,29 +35,35 @@ export interface SearchResultToken {
certified?: boolean; certified?: boolean;
} }
export interface SearchResultAddressOrContract { type SearchResultEnsInfo = {
type: 'address' | 'contract';
name: string | null;
address: string;
is_smart_contract_verified: boolean;
certified?: true;
filecoin_robust_address?: string | null;
url?: string; // not used by the frontend, we build the url ourselves
ens_info?: {
address_hash: string; address_hash: string;
expiry_date?: string; expiry_date?: string;
name: string; name: string;
names_count: number; names_count: number;
}; } | null;
}
export interface SearchResultDomain { interface SearchResultAddressData {
type: 'ens_domain';
name: string | null; name: string | null;
address: string; address: string;
filecoin_robust_address?: string | null;
is_smart_contract_verified: boolean; is_smart_contract_verified: boolean;
certified?: true;
filecoin_robust_address?: string | null;
url?: string; // not used by the frontend, we build the url ourselves url?: string; // not used by the frontend, we build the url ourselves
}
export interface SearchResultAddressOrContract extends SearchResultAddressData {
type: 'address' | 'contract';
ens_info?: SearchResultEnsInfo;
}
export interface SearchResultMetadataTag extends SearchResultAddressData {
type: 'metadata_tag';
ens_info?: SearchResultEnsInfo;
metadata: AddressMetadataTagApi;
}
export interface SearchResultDomain extends SearchResultAddressData {
type: 'ens_domain';
ens_info: { ens_info: {
address_hash: string; address_hash: string;
expiry_date?: string; expiry_date?: string;
...@@ -90,8 +111,16 @@ export interface SearchResultUserOp { ...@@ -90,8 +111,16 @@ export interface SearchResultUserOp {
url?: string; // not used by the frontend, we build the url ourselves url?: string; // not used by the frontend, we build the url ourselves
} }
export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp | export type SearchResultItem =
SearchResultBlob | SearchResultDomain; SearchResultToken |
SearchResultAddressOrContract |
SearchResultBlock |
SearchResultTx |
SearchResultLabel |
SearchResultUserOp |
SearchResultBlob |
SearchResultDomain |
SearchResultMetadataTag;
export interface SearchResult { export interface SearchResult {
items: Array<SearchResultItem>; items: Array<SearchResultItem>;
......
...@@ -20,6 +20,7 @@ test.describe('search by name', () => { ...@@ -20,6 +20,7 @@ test.describe('search by name', () => {
searchMock.token2, searchMock.token2,
searchMock.contract1, searchMock.contract1,
searchMock.address2, searchMock.address2,
searchMock.metatag1,
searchMock.label1, searchMock.label1,
], ],
next_page_params: null, next_page_params: null,
...@@ -52,6 +53,23 @@ test('search by address hash +@mobile', async({ render, mockApiResponse }) => { ...@@ -52,6 +53,23 @@ test('search by address hash +@mobile', async({ render, mockApiResponse }) => {
await expect(component.locator('main')).toHaveScreenshot(); await expect(component.locator('main')).toHaveScreenshot();
}); });
test('search by meta tag +@mobile', async({ render, mockApiResponse }) => {
const hooksConfig = {
router: {
query: { q: 'utko' },
},
};
const data = {
items: [ searchMock.metatag1, searchMock.metatag2, searchMock.metatag3 ],
next_page_params: null,
};
await mockApiResponse('search', data, { queryParams: { q: 'utko' } });
const component = await render(<SearchResults/>, { hooksConfig });
await expect(component.locator('main')).toHaveScreenshot();
});
test('search by block number +@mobile', async({ render, mockApiResponse }) => { test('search by block number +@mobile', async({ render, mockApiResponse }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
......
...@@ -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 { SEARCH_RESULT_TYPES } from 'types/api/search';
import type { SearchResultItem } from 'types/client/search'; import type { SearchResultItem } from 'types/client/search';
import config from 'configs/app'; import config from 'configs/app';
...@@ -95,6 +96,9 @@ const SearchResultsPageContent = () => { ...@@ -95,6 +96,9 @@ const SearchResultsPageContent = () => {
const displayedItems: Array<SearchResultItem | SearchResultAppItem> = React.useMemo(() => { const displayedItems: Array<SearchResultItem | SearchResultAppItem> = React.useMemo(() => {
const apiData = (data?.items || []).filter((item) => { const apiData = (data?.items || []).filter((item) => {
if (!SEARCH_RESULT_TYPES[item.type]) {
return false;
}
if (!config.features.userOps.isEnabled && item.type === 'user_operation') { if (!config.features.userOps.isEnabled && item.type === 'user_operation') {
return false; return false;
} }
......
...@@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; ...@@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import EntityTagIcon from 'ui/shared/EntityTags/EntityTagIcon';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address'; import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -79,6 +80,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ...@@ -79,6 +80,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
); );
} }
case 'metadata_tag':
case 'contract': case 'contract':
case 'address': { case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
...@@ -357,13 +359,16 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ...@@ -357,13 +359,16 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
</Text> </Text>
); );
} }
case 'metadata_tag':
case 'contract': case 'contract':
case 'address': { case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
const addressName = data.name || data.ens_info?.name; const addressName = data.name || data.ens_info?.name;
const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : '';
return addressName ? ( return (addressName || data.type === 'metadata_tag') ? (
<Flex alignItems="center" gap={ 2 } justifyContent="space-between" flexWrap="wrap">
{ addressName && (
<Flex alignItems="center"> <Flex alignItems="center">
<Text <Text
overflow="hidden" overflow="hidden"
...@@ -379,6 +384,15 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ...@@ -379,6 +384,15 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
</Text> </Text>
{ data.certified && <ContractCertifiedLabel iconSize={ 4 } boxSize={ 4 } ml={ 1 }/> } { data.certified && <ContractCertifiedLabel iconSize={ 4 } boxSize={ 4 } ml={ 1 }/> }
</Flex> </Flex>
) }
{ data.type === 'metadata_tag' && (
// we show regular tag because we don't need all meta info here, but need to highlight search term
<Tag display="flex" alignItems="center">
<EntityTagIcon data={ data.metadata }/>
<span dangerouslySetInnerHTML={{ __html: highlightText(data.metadata.name, searchTerm) }}/>
</Tag>
) }
</Flex>
) : ) :
null; null;
} }
......
...@@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; ...@@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import EntityTagIcon from 'ui/shared/EntityTags/EntityTagIcon';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address'; import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -99,6 +100,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -99,6 +100,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
); );
} }
case 'metadata_tag':
case 'contract': case 'contract':
case 'address': { case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
...@@ -119,7 +121,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -119,7 +121,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
return ( return (
<> <>
<Td fontSize="sm" colSpan={ addressName ? 1 : 3 }> <Td fontSize="sm" colSpan={ (addressName || data.type === 'metadata_tag') ? 1 : 3 } verticalAlign="middle">
<AddressEntity.Container> <AddressEntity.Container>
<AddressEntity.Icon address={ address }/> <AddressEntity.Icon address={ address }/>
<AddressEntity.Link <AddressEntity.Link
...@@ -138,7 +140,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -138,7 +140,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
</AddressEntity.Container> </AddressEntity.Container>
</Td> </Td>
{ addressName && ( { addressName && (
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle"> <Td colSpan={ data.type === 'metadata_tag' ? 1 : 2 } fontSize="sm" verticalAlign="middle">
<Flex alignItems="center"> <Flex alignItems="center">
<Text <Text
overflow="hidden" overflow="hidden"
...@@ -159,6 +161,17 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -159,6 +161,17 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
</Flex> </Flex>
</Td> </Td>
) } ) }
{ data.type === 'metadata_tag' && (
<Td colSpan={ addressName ? 1 : 2 } fontSize="sm" verticalAlign="middle">
<Flex justifyContent="flex-end">
{ /* we show regular tag because we don't need all meta info here, but need to highlight search term */ }
<Tag display="flex" alignItems="center">
<EntityTagIcon data={ data.metadata } iconColor="gray.400"/>
<span dangerouslySetInnerHTML={{ __html: highlightText(data.metadata.name, searchTerm) }}/>
</Tag>
</Flex>
</Td>
) }
</> </>
); );
} }
......
import type { ResponsiveValue } from '@chakra-ui/react'; import type { ResponsiveValue } from '@chakra-ui/react';
import { chakra, Image, Tag } from '@chakra-ui/react'; import { Tag } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { EntityTag as TEntityTag } from './types'; import type { EntityTag as TEntityTag } from './types';
import Skeleton from 'ui/shared/chakra/Skeleton'; import Skeleton from 'ui/shared/chakra/Skeleton';
import IconSvg from 'ui/shared/IconSvg';
import TruncatedValue from 'ui/shared/TruncatedValue'; import TruncatedValue from 'ui/shared/TruncatedValue';
import EntityTagIcon from './EntityTagIcon';
import EntityTagLink from './EntityTagLink'; import EntityTagLink from './EntityTagLink';
import EntityTagPopover from './EntityTagPopover'; import EntityTagPopover from './EntityTagPopover';
import { getTagLinkParams } from './utils'; import { getTagLinkParams } from './utils';
...@@ -26,7 +26,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => { ...@@ -26,7 +26,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => {
} }
const hasLink = !noLink && Boolean(getTagLinkParams(data)); const hasLink = !noLink && Boolean(getTagLinkParams(data));
const iconColor = data.meta?.textColor ?? 'gray.400';
const name = (() => { const name = (() => {
if (data.meta?.warpcastHandle) { if (data.meta?.warpcastHandle) {
...@@ -36,22 +35,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => { ...@@ -36,22 +35,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => {
return data.name; return data.name;
})(); })();
const icon = (() => {
if (data.meta?.tagIcon) {
return <Image boxSize={ 3 } mr={ 1 } flexShrink={ 0 } src={ data.meta.tagIcon } alt={ `${ data.name } icon` }/>;
}
if (data.tagType === 'name') {
return <IconSvg name="publictags_slim" boxSize={ 3 } mr={ 1 } flexShrink={ 0 } color={ iconColor }/>;
}
if (data.tagType === 'protocol' || data.tagType === 'generic') {
return <chakra.span color={ iconColor } whiteSpace="pre"># </chakra.span>;
}
return null;
})();
return ( return (
<EntityTagPopover data={ data }> <EntityTagPopover data={ data }>
<Tag <Tag
...@@ -66,7 +49,7 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => { ...@@ -66,7 +49,7 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => {
_hover={ hasLink ? { opacity: 0.76 } : undefined } _hover={ hasLink ? { opacity: 0.76 } : undefined }
> >
<EntityTagLink data={ data } noLink={ noLink }> <EntityTagLink data={ data } noLink={ noLink }>
{ icon } <EntityTagIcon data={ data } iconColor={ data.meta?.textColor }/>
<TruncatedValue value={ name } tooltipPlacement="top"/> <TruncatedValue value={ name } tooltipPlacement="top"/>
</EntityTagLink> </EntityTagLink>
</Tag> </Tag>
......
import { chakra, Image } from '@chakra-ui/react';
import React from 'react';
import type { EntityTag as TEntityTag } from './types';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
data: TEntityTag;
iconColor?: string;
}
const EntityTagIcon = ({ data, iconColor = 'gray.400' }: Props) => {
if (data.meta?.tagIcon) {
return <Image boxSize={ 3 } mr={ 1 } flexShrink={ 0 } src={ data.meta.tagIcon } alt={ `${ data.name } icon` }/>;
}
if (data.tagType === 'name') {
return <IconSvg name="publictags_slim" boxSize={ 3 } mr={ 1 } flexShrink={ 0 } color={ iconColor }/>;
}
if (data.tagType === 'protocol' || data.tagType === 'generic') {
return <chakra.span color={ iconColor } whiteSpace="pre"># </chakra.span>;
}
return null;
};
export default React.memo(EntityTagIcon);
...@@ -53,7 +53,8 @@ export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleSh ...@@ -53,7 +53,8 @@ export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleSh
export function getItemCategory(item: SearchResultItem | SearchResultAppItem): Category | undefined { export function getItemCategory(item: SearchResultItem | SearchResultAppItem): Category | undefined {
switch (item.type) { switch (item.type) {
case 'address': case 'address':
case 'contract': { case 'contract':
case 'metadata_tag': {
return 'address'; return 'address';
} }
case 'token': { case 'token': {
......
...@@ -76,6 +76,19 @@ test('search by address hash +@mobile', async({ render, page, mockApiResponse }) ...@@ -76,6 +76,19 @@ test('search by address hash +@mobile', async({ render, page, mockApiResponse })
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } });
}); });
test('search by meta tag +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => {
const apiUrl = await mockApiResponse('quick_search', [
searchMock.metatag1,
searchMock.metatag2,
searchMock.metatag3,
], { queryParams: { q: 'utko' } });
await render(<SearchBar/>);
await page.getByPlaceholder(/search/i).fill('utko');
await page.waitForResponse(apiUrl);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } });
});
test('search by block number +@mobile', async({ render, page, mockApiResponse }) => { test('search by block number +@mobile', async({ render, page, mockApiResponse }) => {
const apiUrl = await mockApiResponse('quick_search', [ const apiUrl = await mockApiResponse('quick_search', [
searchMock.block1, searchMock.block1,
......
...@@ -142,7 +142,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -142,7 +142,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
return ( return (
<> <>
{ resultCategories.length > 1 && ( { resultCategories.length > 1 && (
<Box position="sticky" top="0" width="100%" background={ bgColor } py={ 5 } my={ -5 } ref={ tabsRef }> <Box position="sticky" top="0" width="100%" background={ bgColor } py={ 5 } my={ -5 } ref={ tabsRef } zIndex={ 1 }>
<Tabs variant="outline" colorScheme="gray" size="sm" index={ tabIndex }> <Tabs variant="outline" colorScheme="gray" size="sm" index={ tabIndex }>
<TabList columnGap={ 3 } rowGap={ 2 } flexWrap="wrap"> <TabList columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
{ resultCategories.map((cat, index) => ( { resultCategories.map((cat, index) => (
......
import { chakra, Box, Text, Flex } from '@chakra-ui/react'; import { chakra, Box, Text, Flex, Tag, Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types'; import type { ItemsProps } from './types';
import type { SearchResultAddressOrContract } from 'types/api/search'; import type { SearchResultAddressOrContract, SearchResultMetadataTag } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32'; import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EntityTagIcon from 'ui/shared/EntityTags/EntityTagIcon';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address'; import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultAddressOrContract>) => { type Props = ItemsProps<SearchResultAddressOrContract | SearchResultMetadataTag>;
const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: Props) => {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address); const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
...@@ -49,6 +52,13 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: ...@@ -49,6 +52,13 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }:
{ data.certified && <ContractCertifiedLabel boxSize={ 4 } iconSize={ 4 } ml={ 1 }/> } { data.certified && <ContractCertifiedLabel boxSize={ 4 } iconSize={ 4 } ml={ 1 }/> }
</Flex> </Flex>
); );
const tagEl = data.type === 'metadata_tag' ? (
// we show regular tag because we don't need all meta info here, but need to highlight search term
<Tag display="flex" alignItems="center" ml={{ base: 0, lg: 'auto' }}>
<EntityTagIcon data={ data.metadata } iconColor="gray.400"/>
<span dangerouslySetInnerHTML={{ __html: highlightText(data.metadata.name, searchTerm) }}/>
</Tag>
) : null;
const addressEl = <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>; const addressEl = <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>;
if (isMobile) { if (isMobile) {
...@@ -66,14 +76,17 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: ...@@ -66,14 +76,17 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }:
{ addressEl } { addressEl }
</Box> </Box>
</Flex> </Flex>
<Flex alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={ 2 }>
{ nameEl } { nameEl }
{ tagEl }
</Flex>
</> </>
); );
} }
return ( return (
<Flex alignItems="center"> <Grid templateColumns="repeat(2, minmax(0, 1fr))" gap={ 2 }>
<Flex alignItems="center" w="450px" mr={ 2 }> <Flex alignItems="center" mr={ 2 } minWidth={ 0 }>
{ icon } { icon }
<Box <Box
as={ shouldHighlightHash ? 'mark' : 'span' } as={ shouldHighlightHash ? 'mark' : 'span' }
...@@ -81,12 +94,16 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: ...@@ -81,12 +94,16 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }:
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
fontWeight={ 700 } fontWeight={ 700 }
minWidth={ 0 }
> >
{ addressEl } { addressEl }
</Box> </Box>
</Flex> </Flex>
<Flex alignItems="center" justifyContent="space-between" gap={ 2 } minWidth={ 0 }>
{ nameEl } { nameEl }
{ tagEl }
</Flex> </Flex>
</Grid>
); );
}; };
......
...@@ -35,7 +35,8 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick, addressForm ...@@ -35,7 +35,8 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick, addressForm
} }
case 'contract': case 'contract':
case 'address': case 'address':
case 'label': { case 'label':
case 'metadata_tag': {
return route({ pathname: '/address/[hash]', query: { hash: data.address } }); return route({ pathname: '/address/[hash]', query: { hash: data.address } });
} }
case 'transaction': { case 'transaction': {
...@@ -73,6 +74,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick, addressForm ...@@ -73,6 +74,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick, addressForm
/> />
); );
} }
case 'metadata_tag':
case 'contract': case 'contract':
case 'address': { case 'address': {
return ( return (
......
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