diff --git a/.github/workflows/test.e2e.yml b/.github/workflows/test.e2e.yml index 405129c66..3b925e01d 100644 --- a/.github/workflows/test.e2e.yml +++ b/.github/workflows/test.e2e.yml @@ -12,7 +12,7 @@ on: jobs: e2e_tests: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 diff --git a/components/AccountOverview/index.tsx b/components/AccountOverview/index.tsx index 8129340a3..2b0bd0ae7 100644 --- a/components/AccountOverview/index.tsx +++ b/components/AccountOverview/index.tsx @@ -265,7 +265,7 @@ const AccountOverview: React.FC Promise< }) => { const [t] = useTranslation(['account', 'common']) - if (!account) { + if (isOverviewLoading) { return (
{ }) }) - it('should redirect to tokens page when keyword is not a number', () => { + it('should redirect to search tokens result page when keyword is not a number', () => { const UNKNOWN_STRING = 'unknown' cy.get(`${ROOT_SELECTOR} input`).type(UNKNOWN_STRING) cy.get(ROOT_SELECTOR).type('{enter}') - cy.url({ timeout: REDIRECT_TIMEOUT }).should('contain', `/tokens/native`) - cy.location('search').should('eq', `?name=${UNKNOWN_STRING}&search=${UNKNOWN_STRING}`) + cy.url({ timeout: REDIRECT_TIMEOUT }).should('contain', `/search-result-tokens`) + cy.location('search').should('eq', `?search=${UNKNOWN_STRING}`) }) it('404', () => { diff --git a/pages/account/[id].tsx b/pages/account/[id].tsx index 8d3033b06..00358194e 100644 --- a/pages/account/[id].tsx +++ b/pages/account/[id].tsx @@ -264,7 +264,7 @@ const Account = () => { accountType ? ( t(`accountType.${accountType}`) ) : ( - t(`accountType.Unknown`) + t(`accountType.UNKNOWN`) ) ) : ( diff --git a/pages/search-result-tokens/index.tsx b/pages/search-result-tokens/index.tsx new file mode 100644 index 000000000..b43265c4d --- /dev/null +++ b/pages/search-result-tokens/index.tsx @@ -0,0 +1,222 @@ +import type { GetStaticProps } from 'next' +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' +import NextLink from 'next/link' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' +import { gql } from 'graphql-request' +import { useQuery } from 'react-query' +import { Skeleton } from '@mui/material' +import SubpageHead from 'components/SubpageHead' +import Pagination from 'components/SimplePagination' +import Table from 'components/Table' +import PageTitle from 'components/PageTitle' +import HashLink from 'components/HashLink' +import TokenLogo from 'components/TokenLogo' +import Tooltip from 'components/Tooltip' +import { SIZES } from 'components/PageSize' +import NoDataIcon from 'assets/icons/no-data.svg' +import { GraphQLSchema, client } from 'utils' +import styles from './styles.module.scss' + +type SearchUdtListProps = { + search_udt: { + entries: Array<{ + id: string + contract_address_hash: string | null + name: string + symbol: string + type: GraphQLSchema.UdtType + icon: string | null + eth_type: GraphQLSchema.TokenType + }> + metadata: GraphQLSchema.PageMetadata + } +} +interface Variables { + before: string | null + after: string | null + name: string + limit: number | null + address: string | null +} +// TODO: add sorter after backend support + +const searchUdtListQuery = gql` + query search_udt($limit: Int, $name: String, $before: String, $after: String, $address: String) { + search_udt( + input: { limit: $limit, fuzzy_name: $name, before: $before, after: $after, contract_address: $address } + ) { + entries { + id + name + symbol + icon + contract_address_hash + eth_type + type + } + metadata { + total_count + after + before + } + } + } +` + +const fetchSearchUdtList = (variables: Variables): Promise => + client + .request(searchUdtListQuery, variables) + .then(data => data.search_udt) + .catch(error => { + console.error(error) + return { + entries: [], + metadata: { + total_count: 0, + before: null, + after: null, + }, + } + }) + +const SearchUdtResultList = () => { + const [t] = useTranslation(['nft', 'common', 'list']) + const { + query: { before = null, after = null, search = null, address = null, page_size = SIZES[1] }, + } = useRouter() + + const title = t(`search_result_title`, { name: search, ns: 'list' }) + const { isLoading, data: list } = useQuery( + ['search-udt-list', page_size, before, after, search, address], + () => + fetchSearchUdtList({ + before: before as string, + after: after as string, + name: search ? `${search}%` : null, + limit: Number.isNaN(+page_size) ? +SIZES[1] : +page_size, + address: address as string, + }), + { refetchInterval: 10000 }, + ) + + const handleTokenLink = (type: GraphQLSchema.TokenType, tokenId: string, contractAddr?: string) => { + switch (type) { + case GraphQLSchema.TokenType.ERC20: + return `/token/${tokenId}` + case GraphQLSchema.TokenType.ERC721: + return `/nft-item/${contractAddr}/${tokenId}` + case GraphQLSchema.TokenType.ERC1155: + return `/multi-token-item/${contractAddr}/${tokenId}` + default: + return '' + } + } + + return ( + <> + +
+ {title} +
+
+ + {t(`n_kinds_in_total`, { + ns: 'list', + number: list?.metadata.total_count.toLocaleString('en') ?? '-', + })} + + {list?.metadata.total_count ? : null} +
+ + + + + + + + + + {list?.metadata.total_count ? ( + list.entries.map(item => { + const { id, eth_type, contract_address_hash, name, icon, symbol } = item + const tokenLink = handleTokenLink(eth_type, id, contract_address_hash) + return ( + + {tokenLink ? ( + + ) : ( + + )} + + {contract_address_hash ? ( + + ) : ( + + )} + + ) + }) + ) : isLoading ? ( + Array.from({ length: +page_size }).map((_, idx) => ( + + + + )) + ) : ( + + + + )} + +
{t('token', { ns: 'list' })}{t('type', { ns: 'list' })}{t('address')}
+ + + + + {name ?? '-'} + {symbol ? `(${symbol})` : ''} + + + + + + + + {name ?? '-'} + {symbol ? `(${symbol})` : ''} + + + {t(eth_type as string, { ns: 'account' })} + + + + + + + -
+ +
+
+ + {t(`no_records`, { ns: 'list' })} +
+
+ {list?.metadata.total_count ? : null} +
+
+ + ) +} + +export const getStaticProps: GetStaticProps = async ({ locale }) => { + const lng = await serverSideTranslations(locale, ['common', 'nft', 'list']) + return { props: lng } +} + +SearchUdtResultList.displayName = 'SearchUdtResultList' + +export default SearchUdtResultList diff --git a/pages/search-result-tokens/styles.module.scss b/pages/search-result-tokens/styles.module.scss new file mode 100644 index 000000000..68b7c073a --- /dev/null +++ b/pages/search-result-tokens/styles.module.scss @@ -0,0 +1,106 @@ +@import '../../styles/mixin.scss'; + +.container { + @include main-center; + form[data-role='filter-menu']:first-of-type { + left: 0; + transform: translateX(-20px); + } + &[data-is-filter-unnecessary='true'] { + th { + svg { + display: none; + } + } + } +} + +.subheader { + display: flex; + justify-content: space-between; + align-items: center; + height: 64px; + padding: 0 1.5rem; + font-size: 1rem; + font-weight: 500; + & > div { + padding: 0; + height: auto; + & > div:first-child { + display: none; + } + } + @media screen and (max-width: 600px) { + flex-direction: column; + font-size: 0.875rem; + height: 5.5rem; + padding: 1.125rem 0.75rem 0.75rem; + align-items: stretch; + } +} + +.tokenHeader { + width: 33%; + @media screen and (max-width: 600px) { + width: auto; + } +} + +.token { + display: flex; + align-items: center; + span { + margin-left: 0.5rem; + } +} + +.typeHeader { + width: 33%; + @media screen and (max-width: 600px) { + width: auto; + } +} + +.addr { + a:last-child { + display: none; + } + @media screen and (max-width: 1024px) { + a:first-child { + display: none; + } + a:last-child { + display: inline; + } + } +} + +.list { + margin-top: 1.5rem; + background-color: #fff; + border: 1px solid var(--border-color); + border-radius: 1rem; + font-size: 0.875rem; + + td { + padding-top: 17px !important; + padding-bottom: 17px !important; + } + + @media screen and (max-width: 1024px) { + border-radius: 0.5rem; + margin-top: 1rem; + } +} + +.sorter { + cursor: pointer; + + &[data-order='DESC'] { + transform: rotate(0.5turn); + } +} + +.noRecords { + @include empty-list; +} diff --git a/pages/token/[id].tsx b/pages/token/[id].tsx index f2d9c310a..df43092ea 100644 --- a/pages/token/[id].tsx +++ b/pages/token/[id].tsx @@ -52,7 +52,6 @@ interface TokenInfoProps { description: string | null supply: string holders_count: number - minted_count: number contract_address_hash: string token_exchange_rate: { exchange_rate: number | null @@ -77,7 +76,6 @@ const tokenInfoQuery = gql` description supply holders_count - minted_count contract_address_hash token_exchange_rate { exchange_rate diff --git a/public/locales/en-US/list.json b/public/locales/en-US/list.json index 1fb86ce78..8cf16b0bf 100644 --- a/public/locales/en-US/list.json +++ b/public/locales/en-US/list.json @@ -94,5 +94,6 @@ "price": "Price", "price-updated-at": "Price updated at {{time}}", "switch-to-price": "Switch to price", - "switch-to-amount": "Switch to amount" + "switch-to-amount": "Switch to amount", + "search_result_title": "Search results for the token name \"{{name}}\"" } diff --git a/public/locales/zh-CN/list.json b/public/locales/zh-CN/list.json index 9a6602d64..7439fd85b 100644 --- a/public/locales/zh-CN/list.json +++ b/public/locales/zh-CN/list.json @@ -93,5 +93,6 @@ "price": "价格", "price-updated-at": "价格更新于 {{time}}", "switch-to-price": "切换到价格", - "switch-to-amount": "切换到数量" + "switch-to-amount": "切换到数量", + "search_result_title": "代币名 \"{{name}}\" 搜索结果" } diff --git a/utils/api/search.ts b/utils/api/search.ts index f93c08ad0..eda19fabd 100644 --- a/utils/api/search.ts +++ b/utils/api/search.ts @@ -1,7 +1,34 @@ import { addressToScript } from '@nervosnetwork/ckb-sdk-utils' -import { isError, ErrorResponse, API_ENDPOINT, HttpStatus } from './utils' +import { gql } from 'graphql-request' +import { client } from 'utils/graphql' -export const fetchSearch = (search: string) => { +const searchKeywordQuery = gql` + query searchKeyword($text: String!) { + search_keyword(input: { keyword: $text }) { + type + id + } + } +` + +interface Variables { + text: string +} + +type SearchKeywordProps = { + search_keyword: { + type: string | null + id: string | null + } +} + +const fetchSearchResult = (variables: Variables): Promise => + client + .request(searchKeywordQuery, variables) + .then(data => data.search_keyword) + .catch(() => ({ type: null, id: null })) + +export const fetchSearchKeyword = (search: string) => { let query = search if (query.startsWith('ck')) { try { @@ -11,28 +38,26 @@ export const fetchSearch = (search: string) => { console.warn(err) } } - - return fetch(`${API_ENDPOINT}/search?keyword=${query}`) + return fetchSearchResult({ text: query }) .then(async res => { - if (res.status === HttpStatus.NotFound) { + if (!res || !res.id) { return `/404` } - const found: Record<'id' | 'type', string> | ErrorResponse = await res.json() - if (isError(found)) { - return `/404` - } - switch (found.type) { - case 'block': { - return `/block/${found.id}` + switch (res.type) { + case 'BLOCK': { + return `/block/${res.id}` + } + case 'TRANSACTION': { + return `/tx/${res.id}` } - case 'transaction': { - return `/tx/${found.id}` + case 'ACCOUNT': { + return `/account/${res.id}` } - case 'account': { - return `/account/${found.id}` + case 'ADDRESS': { + return `/account/${res.id}` } - case 'udt': { - return `/token/${found.id}` + case 'UDT': { + return `/token/${res.id}` } default: { return `/404` diff --git a/utils/handler.ts b/utils/handler.ts index 45894ead9..9cd0d62e6 100644 --- a/utils/handler.ts +++ b/utils/handler.ts @@ -1,6 +1,6 @@ import { ServerResponse } from 'http' import { NotFoundException } from './exceptions' -import { fetchSearch } from './api' +import { fetchSearchKeyword } from './api' export const handleApiError = (err: Error, _res: ServerResponse | null, locale: string, query?: string) => { // return { @@ -34,14 +34,18 @@ export const handleSearchKeyPress = async (e: React.KeyboardEvent