Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add search result tokens page #816

Merged
merged 11 commits into from
Jan 6, 2023
6 changes: 3 additions & 3 deletions cypress/integration/search/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ context('Search', () => {
})
})

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', () => {
Expand Down
225 changes: 225 additions & 0 deletions pages/search-result-tokens/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
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<SearchUdtListProps['search_udt']> =>
client
.request<SearchUdtListProps>(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 as string).trim() : 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 (
<>
<SubpageHead subtitle={title} />
<div className={styles.container}>
<PageTitle>{title}</PageTitle>
<div className={styles.list}>
<div className={styles.subheader}>
<span>
{t(`n_kinds_in_total`, {
ns: 'list',
number: list?.metadata.total_count.toLocaleString('en') ?? '-',
})}
</span>
{list?.metadata.total_count ? <Pagination {...list.metadata} /> : null}
</div>
<Table>
<thead>
<tr>
<th className={styles.tokenHeader}>{t('token', { ns: 'list' })}</th>
<th className={styles.typeHeader}>{t('type', { ns: 'list' })}</th>
<th>{t('address')} </th>
</tr>
</thead>
<tbody>
{list?.metadata.total_count ? (
list.entries.map(item => {
return (
<tr key={item.id}>
{handleTokenLink(item.eth_type, item.id, item.contract_address_hash) ? (
qiweiii marked this conversation as resolved.
Show resolved Hide resolved
<td title={item.name}>
<NextLink href={handleTokenLink(item.eth_type, item.id, item.contract_address_hash)}>
<a className={styles.token}>
<TokenLogo name={item.name} logo={item.icon} />
<span>
{item.name ?? '-'}
{item.symbol ? `(${item.symbol})` : ''}
</span>
</a>
</NextLink>
</td>
) : (
<td title={item.name}>
<span className={styles.token}>
<TokenLogo name={item.name} logo={item.icon} />
<span>
{item.name ?? '-'}
{item.symbol ? `(${item.symbol})` : ''}
</span>
</span>
</td>
)}
<td className={styles.type}>{t(item.eth_type as string, { ns: 'account' })}</td>
{item?.contract_address_hash ? (
<td className={styles.addr} title={item.contract_address_hash}>
qiweiii marked this conversation as resolved.
Show resolved Hide resolved
<HashLink
label={item.contract_address_hash}
href={`/account/${item.contract_address_hash}`}
/>
<Tooltip title={item.contract_address_hash} placement="top">
<span>
<HashLink
label={`${item.contract_address_hash.slice(0, 8)}...${item.contract_address_hash.slice(
-8,
)}`}
href={`/account/${item.contract_address_hash}`}
/>
</span>
</Tooltip>
</td>
) : (
<td className={styles.addr}>-</td>
)}
</tr>
)
})
) : isLoading ? (
Array.from({ length: +page_size }).map((_, idx) => (
<tr key={idx}>
<td colSpan={3}>
<Skeleton animation="wave" />
</td>
</tr>
))
) : (
<tr>
<td colSpan={3}>
<div className={styles.noRecords}>
<NoDataIcon />
<span>{t(`no_records`, { ns: 'list' })}</span>
</div>
</td>
</tr>
)}
</tbody>
</Table>
{list?.metadata.total_count ? <Pagination {...list.metadata} /> : null}
</div>
</div>
</>
)
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
const lng = await serverSideTranslations(locale, ['common', 'nft', 'list'])
return { props: lng }
}

SearchUdtResultList.displayName = 'SearchUdtResultList'

export default SearchUdtResultList
106 changes: 106 additions & 0 deletions pages/search-result-tokens/styles.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 0 additions & 2 deletions pages/token/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -77,7 +76,6 @@ const tokenInfoQuery = gql`
description
supply
holders_count
minted_count
contract_address_hash
token_exchange_rate {
exchange_rate
Expand Down
3 changes: 2 additions & 1 deletion public/locales/en-US/list.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,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}}\""
}
3 changes: 2 additions & 1 deletion public/locales/zh-CN/list.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,6 @@
"price": "价格",
"price-updated-at": "价格更新于 {{time}}",
"switch-to-price": "切换到价格",
"switch-to-amount": "切换到数量"
"switch-to-amount": "切换到数量",
"search_result_title": "代币名 \"{{name}}\" 搜索结果"
}
Loading