Skip to content

Commit

Permalink
feat: support aggregate search (#296)
Browse files Browse the repository at this point in the history
  • Loading branch information
PainterPuppets authored May 18, 2024
1 parent 675f86f commit 509e3ca
Show file tree
Hide file tree
Showing 14 changed files with 781 additions and 296 deletions.
144 changes: 144 additions & 0 deletions src/components/Search/AggregateSearchResults.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
.searchResultsPanelWrapper {
position: absolute;
z-index: 2;
top: calc(100% + 8px);
left: 0;
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-height: 292px;
overflow-y: auto;
background: white;
color: black;
border-radius: 4px;
box-shadow: 0 4px 4px 0 rgb(16 16 16 / 5%);
}

.ellipsisText {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.highlight {
color: var(--primary-color);
}

.empty {
padding: 28px 0;
text-align: center;
font-size: 16px;
color: #333;
}

.searchResult {
display: block;
width: 100%;
padding: 4px 0;
cursor: pointer;
border-bottom: solid 1px #e5e5e5;

&:last-child {
border-bottom: none;
}

&:hover {
.content,
.boxContent {
background: #f5f5f5;
}
}
}

.boxContent {
display: flex;
flex-direction: column;
align-items: flex-start;
overflow: hidden;
width: 100%;
padding: 4px;
border-radius: 4px;
}

.content {
display: flex;
align-items: center;
overflow: hidden;
width: 100%;
padding: 4px;
border-radius: 4px;
}

.tokenSymbol {
font-size: 16px;
color: #333;
}

.secondaryText {
font-size: 14px;
color: #666;
}

.subTitle {
display: flex;
width: 100%;
overflow: hidden;
align-items: center;
}

.categoryTitle {
color: #666;
font-size: 0.65rem;
letter-spacing: 0.5px;
font-weight: 700;
padding: 12px 12px 6px;
background-color: #f5f5f5;
}

.categoryList {
padding: 6px 8px;
}

.icon {
display: inline-flex;
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 6px;
flex-shrink: 0;
object-fit: cover;
}

.searchCategoryFilter {
display: flex;
flex-wrap: wrap;
padding: 12px 12px 0;
gap: 4px;
}

.searchCategoryTag {
border: 1px solid #e5e5e5;
border-radius: 24px;
padding: 4px 12px;
cursor: pointer;
transition: all 0.3s;

&.active {
border-color: var(--primary-color);
color: var(--primary-color);
}
}

.rgbPlus {
color: #333;
display: flex;
padding: 4px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #ffd176 0.23%, #ffdb81 6.7%, #84ffcb 99.82%);
vertical-align: middle;
}
225 changes: 225 additions & 0 deletions src/components/Search/AggregateSearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/* eslint-disable react/destructuring-assignment */
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import { FC, useEffect, useState } from 'react'
import { SearchResultType, AggregateSearchResult } from '../../services/ExplorerService'
import { getURLByAggregateSearchResult, getDisplayNameByAggregateSearchResult } from './utils'
import { HighlightText } from './HighlightText'
import { handleNftImgError, patchMibaoImg } from '../../utils/util'
import { localeNumberString } from '../../utils/number'
import styles from './AggregateSearchResults.module.scss'
import EllipsisMiddle from '../EllipsisMiddle'
import SmallLoading from '../Loading/SmallLoading'
import { Link } from '../Link'

type Props = {
keyword?: string
loading?: boolean
results: AggregateSearchResult[]
}

export const AggregateSearchResults: FC<Props> = ({ keyword = '', results, loading }) => {
const { t } = useTranslation()
const [activatedCategory, setActivatedCategory] = useState<SearchResultType | undefined>(undefined)

useEffect(() => {
setActivatedCategory(undefined)
}, [results])

const categories = results.reduce((acc, result) => {
if (!acc[result.type]) {
acc[result.type] = []
}
acc[result.type].push(result)
return acc
}, {} as Record<SearchResultType, AggregateSearchResult[]>)

const SearchResultCategoryPanel = (() => {
return (
<div className={styles.searchResultCategory}>
{Object.entries(categories)
.filter(([type]) => (activatedCategory === undefined ? true : activatedCategory === type))
.map(([type, items]) => (
<div key={type} className={styles.category}>
<div className={styles.categoryTitle}>{t(`search.${type}`)}</div>
<div className={styles.categoryList}>
{items.map(item => (
<SearchResultItem keyword={keyword} key={item.id} item={item} />
))}
</div>
</div>
))}
</div>
)
})()

return (
<div className={styles.searchResultsPanelWrapper}>
{!loading && Object.keys(categories).length > 0 && (
<div className={styles.searchCategoryFilter}>
{(Object.keys(categories) as SearchResultType[]).map(category => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
className={classNames(styles.searchCategoryTag, { [styles.active]: activatedCategory === category })}
onClick={() => setActivatedCategory(pre => (pre === category ? undefined : category))}
>
{t(`search.${category}`)} {`(${categories[category].length})`}
</div>
))}
</div>
)}
{/* eslint-disable-next-line no-nested-ternary */}
{loading ? (
<SmallLoading className={styles.loadingWrapper} />
) : results.length === 0 ? (
<div className={styles.empty}>{t('search.no_search_result')}</div>
) : (
SearchResultCategoryPanel
)}
</div>
)
}

const SearchResultItem: FC<{ keyword?: string; item: AggregateSearchResult }> = ({ item, keyword = '' }) => {
const { t } = useTranslation()
const displayName = getDisplayNameByAggregateSearchResult(item)?.toString()
const to = getURLByAggregateSearchResult(item)
if (!to) return null

if (item.type === SearchResultType.UDT) {
return (
<Link className={styles.searchResult} to={to}>
<div className={styles.boxContent}>
<div style={{ display: 'flex', width: '100%' }}>
{!item.attributes.symbol ? (
t('udt.unknown_token')
) : (
<HighlightText text={item.attributes.symbol} keyword={keyword} style={{ flex: 2, marginRight: 4 }} />
)}
{item.attributes.fullName && (
<span className={classNames(styles.secondaryText, styles.subTitle)} style={{ flex: 1 }}>
(<HighlightText text={item.attributes.fullName} keyword={keyword} style={{ width: '100%' }} />)
</span>
)}
</div>

<div className={classNames(styles.secondaryText, styles.subTitle, 'monospace')}>
<span style={{ marginRight: 4, flexShrink: 0 }}>type hash: </span>
<HighlightText style={{ width: '100%' }} text={item.attributes.typeHash} keyword={keyword} />
</div>
</div>
</Link>
)
}

if (item.type === SearchResultType.TokenCollection) {
return (
<Link className={styles.searchResult} to={to}>
<div className={styles.content}>
{item.attributes.iconUrl ? (
<img
src={`${patchMibaoImg(item.attributes.iconUrl)}?size=small`}
alt="cover"
loading="lazy"
className={styles.icon}
onError={handleNftImgError}
/>
) : (
<img
src={
item.attributes.standard === 'spore' ? '/images/spore_placeholder.svg' : '/images/nft_placeholder.png'
}
alt="cover"
loading="lazy"
className={styles.icon}
/>
)}

<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
{!displayName ? (
t('udt.unknown_token')
) : (
<HighlightText text={displayName} keyword={keyword} style={{ flex: 1 }} />
)}

<div className={classNames(styles.secondaryText, styles.subTitle, 'monospace')}>
<span style={{ marginRight: 4, flexShrink: 0 }}>sn: </span>
<HighlightText style={{ width: '100%' }} text={item.attributes.sn} keyword={keyword} />
</div>
</div>
</div>
</Link>
)
}

if (item.type === SearchResultType.DID) {
return (
<Link className={styles.searchResult} to={to}>
<div className={styles.content}>
<HighlightText
style={{ maxWidth: 'min(200px, 60%)', marginRight: 8 }}
text={item.attributes.did}
keyword={keyword}
/>
<EllipsisMiddle
className={classNames(styles.secondaryText, 'monospace')}
style={{ maxWidth: 'min(200px, 40%)' }}
useTextWidthForPlaceholderWidth
title={item.attributes.address}
>
{item.attributes.address}
</EllipsisMiddle>
</div>
</Link>
)
}

if (item.type === SearchResultType.BtcTx) {
return (
<Link className={styles.searchResult} to={to}>
<div className={styles.boxContent}>
<div className={classNames(styles.subTitle)}>
<HighlightText
text={item.attributes.ckbTransactionHash}
keyword={keyword}
style={{ flex: 1, marginRight: 4 }}
/>
<span className={styles.rgbPlus}>RGB++</span>
</div>
<div className={classNames(styles.secondaryText, styles.subTitle, 'monospace')}>
<span style={{ marginRight: 4, flexShrink: 0 }}>btc id:</span>
<HighlightText style={{ width: '100%' }} text={item.attributes.txid} keyword={keyword} />
</div>
</div>
</Link>
)
}

if (item.type === SearchResultType.Transaction) {
return (
<Link className={styles.searchResult} to={to}>
<div className={styles.boxContent}>
<HighlightText style={{ width: '100%' }} text={item.attributes.transactionHash} keyword={keyword} />

<div className={classNames(styles.secondaryText, styles.subTitle, 'monospace')}>
<span style={{ marginRight: 4, flexShrink: 0 }}>
{t('search.block')} # {localeNumberString(item.attributes.blockNumber)}
</span>
</div>
</div>
</Link>
)
}

return (
<Link className={styles.searchResult} to={to}>
<div className={styles.content}>
{!displayName ? (
t('udt.unknown_token')
) : (
<HighlightText style={{ width: '100%' }} text={displayName} keyword={keyword} />
)}
</div>
</Link>
)
}
9 changes: 9 additions & 0 deletions src/components/Search/HighlightText.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.highlightText {
white-space: nowrap;
overflow: hidden;
display: flex;
}

.highlight {
color: var(--primary-color);
}
Loading

0 comments on commit 509e3ca

Please sign in to comment.