Skip to content

Commit

Permalink
feat: add search result tokens page (#816)
Browse files Browse the repository at this point in the history
* feat: add search result tokens page

* fix: tests

* fix: search_keyword gql call

* fix: address type redirect

* fix: fuzzy search

* refactor: code style and finish loading state

* ci: upgrade to ubuntu-latest
  • Loading branch information
qiweiii authored Jan 6, 2023
1 parent fea6ff2 commit a13bc2b
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:

jobs:
e2e_tests:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion components/AccountOverview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ const AccountOverview: React.FC<AccountOverviewProps & { refetch: () => Promise<
}) => {
const [t] = useTranslation(['account', 'common'])

if (!account) {
if (isOverviewLoading) {
return (
<div className={styles.container}>
<InfoList
Expand Down
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
2 changes: 1 addition & 1 deletion pages/account/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ const Account = () => {
accountType ? (
t(`accountType.${accountType}`)
) : (
t(`accountType.Unknown`)
t(`accountType.UNKNOWN`)
)
) : (
<Skeleton animation="wave" width="200px" />
Expand Down
222 changes: 222 additions & 0 deletions pages/search-result-tokens/index.tsx
Original file line number Diff line number Diff line change
@@ -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<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}%` : 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 => {
const { id, eth_type, contract_address_hash, name, icon, symbol } = item
const tokenLink = handleTokenLink(eth_type, id, contract_address_hash)
return (
<tr key={id}>
{tokenLink ? (
<td title={name}>
<NextLink href={tokenLink}>
<a className={styles.token}>
<TokenLogo name={name} logo={icon} />
<span>
{name ?? '-'}
{symbol ? `(${symbol})` : ''}
</span>
</a>
</NextLink>
</td>
) : (
<td title={name}>
<span className={styles.token}>
<TokenLogo name={name} logo={icon} />
<span>
{name ?? '-'}
{symbol ? `(${symbol})` : ''}
</span>
</span>
</td>
)}
<td className={styles.type}>{t(eth_type as string, { ns: 'account' })}</td>
{contract_address_hash ? (
<td className={styles.addr} title={contract_address_hash}>
<HashLink label={contract_address_hash} href={`/account/${contract_address_hash}`} />
<Tooltip title={contract_address_hash} placement="top">
<span>
<HashLink
label={`${contract_address_hash.slice(0, 8)}...${contract_address_hash.slice(-8)}`}
href={`/account/${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 @@ -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}}\""
}
Loading

0 comments on commit a13bc2b

Please sign in to comment.