diff --git a/mocks/search/index.ts b/mocks/search/index.ts index c0510f0225..f16566c12f 100644 --- a/mocks/search/index.ts +++ b/mocks/search/index.ts @@ -8,6 +8,7 @@ import type { SearchResultUserOp, SearchResultBlob, SearchResultDomain, + SearchResultMetadataTag, } from 'types/api/search'; export const token1: SearchResultToken = { @@ -147,6 +148,42 @@ export const domain1: SearchResultDomain = { 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 = { items: [ token1, @@ -157,6 +194,8 @@ export const baseResponse: SearchResult = { tx1, blob1, domain1, + metatag1, + ], next_page_params: null, }; diff --git a/types/api/search.ts b/types/api/search.ts index 22068c85d5..90526b57c9 100644 --- a/types/api/search.ts +++ b/types/api/search.ts @@ -1,7 +1,22 @@ import type * as bens from '@blockscout/bens-types'; 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 { type: 'token'; @@ -20,29 +35,35 @@ export interface SearchResultToken { certified?: boolean; } -export interface SearchResultAddressOrContract { - type: 'address' | 'contract'; +type SearchResultEnsInfo = { + address_hash: string; + expiry_date?: string; + name: string; + names_count: number; +} | null; + +interface SearchResultAddressData { 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; - expiry_date?: string; - name: string; - names_count: number; - }; } -export interface SearchResultDomain { +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'; - name: string | null; - address: string; - filecoin_robust_address?: string | null; - is_smart_contract_verified: boolean; - url?: string; // not used by the frontend, we build the url ourselves ens_info: { address_hash: string; expiry_date?: string; @@ -90,8 +111,16 @@ export interface SearchResultUserOp { url?: string; // not used by the frontend, we build the url ourselves } -export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp | -SearchResultBlob | SearchResultDomain; +export type SearchResultItem = + SearchResultToken | + SearchResultAddressOrContract | + SearchResultBlock | + SearchResultTx | + SearchResultLabel | + SearchResultUserOp | + SearchResultBlob | + SearchResultDomain | + SearchResultMetadataTag; export interface SearchResult { items: Array; diff --git a/ui/pages/SearchResults.pw.tsx b/ui/pages/SearchResults.pw.tsx index f4724a1748..8959dc22de 100644 --- a/ui/pages/SearchResults.pw.tsx +++ b/ui/pages/SearchResults.pw.tsx @@ -20,6 +20,7 @@ test.describe('search by name', () => { searchMock.token2, searchMock.contract1, searchMock.address2, + searchMock.metatag1, searchMock.label1, ], next_page_params: null, @@ -52,6 +53,23 @@ test('search by address hash +@mobile', async({ render, mockApiResponse }) => { 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(, { hooksConfig }); + + await expect(component.locator('main')).toHaveScreenshot(); +}); + test('search by block number +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { diff --git a/ui/pages/SearchResults.tsx b/ui/pages/SearchResults.tsx index 21d74ddbb9..2628d633ae 100644 --- a/ui/pages/SearchResults.tsx +++ b/ui/pages/SearchResults.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; import type { FormEvent } from 'react'; import React from 'react'; +import { SEARCH_RESULT_TYPES } from 'types/api/search'; import type { SearchResultItem } from 'types/client/search'; import config from 'configs/app'; @@ -95,6 +96,9 @@ const SearchResultsPageContent = () => { const displayedItems: Array = React.useMemo(() => { const apiData = (data?.items || []).filter((item) => { + if (!SEARCH_RESULT_TYPES[item.type]) { + return false; + } if (!config.features.userOps.isEnabled && item.type === 'user_operation') { return false; } diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png index 59842dbc7f..1c1460b364 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-meta-tag-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-meta-tag-mobile-1.png new file mode 100644 index 0000000000..fbcb2bb7d7 Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-meta-tag-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png index b5885bfbfb..d4a8a6b5eb 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-meta-tag-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-meta-tag-mobile-1.png new file mode 100644 index 0000000000..f888e4092f Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-meta-tag-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png index da3a90b01b..ca6cb77854 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/searchResults/SearchResultListItem.tsx b/ui/searchResults/SearchResultListItem.tsx index c1df21a285..77cc982f95 100644 --- a/ui/searchResults/SearchResultListItem.tsx +++ b/ui/searchResults/SearchResultListItem.tsx @@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; 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 HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import IconSvg from 'ui/shared/IconSvg'; @@ -80,6 +81,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ); } + case 'metadata_tag': case 'contract': case 'address': { const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); @@ -367,27 +369,39 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ); } + case 'metadata_tag': case 'contract': case 'address': { const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const addressName = data.name || data.ens_info?.name; const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; - return addressName ? ( - - - - { data.ens_info && ( - data.ens_info.names_count > 1 ? - ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : - { expiresText } - ) } - - { data.certified && } + return (addressName || data.type === 'metadata_tag') ? ( + + { addressName && ( + + + + { data.ens_info && ( + data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } + ) } + + { data.certified && } + + ) } + { data.type === 'metadata_tag' && ( + // we show regular tag because we don't need all meta info here, but need to highlight search term + + + + + ) } ) : null; diff --git a/ui/searchResults/SearchResultTableItem.tsx b/ui/searchResults/SearchResultTableItem.tsx index e1b638f15e..a707068b1a 100644 --- a/ui/searchResults/SearchResultTableItem.tsx +++ b/ui/searchResults/SearchResultTableItem.tsx @@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; 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 HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import IconSvg from 'ui/shared/IconSvg'; @@ -100,6 +101,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ); } + case 'metadata_tag': case 'contract': case 'address': { const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); @@ -120,7 +122,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P return ( <> - + { addressName && ( - + ) } + { data.type === 'metadata_tag' && ( + + + { /* we show regular tag because we don't need all meta info here, but need to highlight search term */ } + + + + + + + ) } ); } diff --git a/ui/shared/EntityTags/EntityTag.tsx b/ui/shared/EntityTags/EntityTag.tsx index f403c2ea58..34b1fe1643 100644 --- a/ui/shared/EntityTags/EntityTag.tsx +++ b/ui/shared/EntityTags/EntityTag.tsx @@ -1,13 +1,13 @@ 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 type { EntityTag as TEntityTag } from './types'; import Skeleton from 'ui/shared/chakra/Skeleton'; -import IconSvg from 'ui/shared/IconSvg'; import TruncatedValue from 'ui/shared/TruncatedValue'; +import EntityTagIcon from './EntityTagIcon'; import EntityTagLink from './EntityTagLink'; import EntityTagPopover from './EntityTagPopover'; import { getTagLinkParams } from './utils'; @@ -26,7 +26,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => { } const hasLink = !noLink && Boolean(getTagLinkParams(data)); - const iconColor = data.meta?.textColor ?? 'gray.400'; const name = (() => { if (data.meta?.warpcastHandle) { @@ -36,22 +35,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => { return data.name; })(); - const icon = (() => { - if (data.meta?.tagIcon) { - return {; - } - - if (data.tagType === 'name') { - return ; - } - - if (data.tagType === 'protocol' || data.tagType === 'generic') { - return # ; - } - - return null; - })(); - return ( { _hover={ hasLink ? { opacity: 0.76 } : undefined } > - { icon } + diff --git a/ui/shared/EntityTags/EntityTagIcon.tsx b/ui/shared/EntityTags/EntityTagIcon.tsx new file mode 100644 index 0000000000..21c19a99a1 --- /dev/null +++ b/ui/shared/EntityTags/EntityTagIcon.tsx @@ -0,0 +1,30 @@ +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 {; + } + + if (data.tagType === 'name') { + return ; + } + + if (data.tagType === 'protocol' || data.tagType === 'generic') { + return # ; + } + + return null; +}; + +export default React.memo(EntityTagIcon); diff --git a/ui/shared/search/utils.ts b/ui/shared/search/utils.ts index 31da3bc447..5144f73924 100644 --- a/ui/shared/search/utils.ts +++ b/ui/shared/search/utils.ts @@ -53,7 +53,8 @@ export const searchItemTitles: Record { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.metatag1, + searchMock.metatag2, + searchMock.metatag3, + ], { queryParams: { q: 'utko' } }); + await render(); + 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 }) => { const apiUrl = await mockApiResponse('quick_search', [ searchMock.block1, diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx index e8f392d441..5d29acb482 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx @@ -142,7 +142,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props return ( <> { resultCategories.length > 1 && ( - + { resultCategories.map((cat, index) => ( diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx index 079dae8011..4624ee09fd 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx @@ -1,18 +1,21 @@ -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 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 dayjs from 'lib/date/dayjs'; import highlightText from 'lib/highlightText'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; 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 HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; -const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps) => { +type Props = ItemsProps; + +const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: Props) => { const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address); @@ -49,6 +52,13 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: { data.certified && } ); + 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 + + + + + ) : null; const addressEl = ; if (isMobile) { @@ -66,14 +76,17 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: { addressEl } - { nameEl } + + { nameEl } + { tagEl } + ); } return ( - - + + { icon } { addressEl } - { nameEl } - + + { nameEl } + { tagEl } + + ); }; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx index a45496dbe7..564f593189 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx @@ -35,7 +35,8 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick, addressForm } case 'contract': case 'address': - case 'label': { + case 'label': + case 'metadata_tag': { return route({ pathname: '/address/[hash]', query: { hash: data.address } }); } case 'transaction': { @@ -73,6 +74,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick, addressForm /> ); } + case 'metadata_tag': case 'contract': case 'address': { return ( diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png index ac86dd52f9..152fc08adf 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-meta-tag-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-meta-tag-mobile-dark-mode-1.png new file mode 100644 index 0000000000..19c6621992 Binary files /dev/null and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-meta-tag-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png index 266acbdea6..671e9ea6b0 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png index 70b94b6670..db61dcc925 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-meta-tag-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-meta-tag-mobile-dark-mode-1.png new file mode 100644 index 0000000000..dc4f0023d9 Binary files /dev/null and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-meta-tag-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-meta-tag-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-meta-tag-mobile-dark-mode-1.png new file mode 100644 index 0000000000..1e94b2b901 Binary files /dev/null and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-meta-tag-mobile-dark-mode-1.png differ