Commit 0c1fa3f7 authored by tom's avatar tom

highlight search term

parent ce6b0349
export default function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
import escapeRegExp from 'lib/escapeRegExp';
export default function highlightText(text: string, query: string) {
const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
......@@ -9,6 +9,10 @@ const global = (props: StyleFunctionProps) => ({
...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent',
},
mark: {
bgColor: 'yellow.200',
color: 'inherit',
},
'svg *::selection': {
color: 'none',
background: 'none',
......
......@@ -2,7 +2,6 @@ import { Box, chakra, Table, Tbody, Tr, Th, Skeleton, Show, Hide } from '@chakra
import type { FormEvent } from 'react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import SearchResultListItem from 'ui/searchResults/SearchResultListItem';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
......@@ -21,7 +20,6 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => {
const { query, searchTerm, handleSearchTermChange } = useSearchQuery(true);
const { data, isError, isLoading, pagination, isPaginationVisible } = query;
const isMobile = useIsMobile();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
......@@ -40,10 +38,10 @@ const SearchResultsPageContent = () => {
return (
<Box>
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ 6 }/>
<Show below="lg" ssr={ false }>
<Show below="lg">
<SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }>
<Hide below="lg">
<SkeletonTable columns={ [ '33%', '34%', '33%' ] }/>
</Hide>
</Box>
......@@ -65,22 +63,14 @@ const SearchResultsPageContent = () => {
return text;
}
if (isMobile) {
return (
<>
{ text }
<ActionBar>
<Pagination { ...pagination } ml="auto"/>
</ActionBar>
</>
);
}
return (
<ActionBar mt={ -6 }>
{ text }
<Pagination { ...pagination }/>
</ActionBar>
<>
<Box display={{ base: 'block', lg: 'none' }}>{ text }</Box>
<ActionBar mt={{ base: 0, lg: -6 }}>
<Box display={{ base: 'none', lg: 'block' }}>{ text }</Box>
<Pagination { ...pagination }/>
</ActionBar>
</>
);
})();
......@@ -90,7 +80,7 @@ const SearchResultsPageContent = () => {
{ data.items.length > 0 && (
<>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => <SearchResultListItem key={ index } data={ item }/>) }
{ data.items.map((item, index) => <SearchResultListItem key={ index } data={ item } searchTerm={ searchTerm }/>) }
</Show>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="md" fontWeight={ 500 }>
......@@ -102,7 +92,7 @@ const SearchResultsPageContent = () => {
</Tr>
</Thead>
<Tbody>
{ data.items.map((item, index) => <SearchResultTableItem key={ index } data={ item }/>) }
{ data.items.map((item, index) => <SearchResultTableItem key={ index } data={ item } searchTerm={ searchTerm }/>) }
</Tbody>
</Table>
</Hide>
......
import { Text, Link, Flex, Icon } from '@chakra-ui/react';
import { Text, Link, Flex, Icon, Box, chakra } from '@chakra-ui/react';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -15,18 +16,21 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
searchTerm: string;
}
const SearchResultListItem = ({ data }: Props) => {
const SearchResultListItem = ({ data, searchTerm }: Props) => {
const firstRow = (() => {
switch (data.type) {
case 'token': {
const name = data.name + (data.symbol ? ` (${ data.symbol })` : '');
return (
<Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name }/>
<Link ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 }>
{ data.name }{ data.symbol ? ` (${ data.symbol })` : '' }
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</Link>
</Flex>
);
......@@ -34,20 +38,24 @@ const SearchResultListItem = ({ data }: Props) => {
case 'contract':
case 'address': {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return (
<Address>
<AddressIcon hash={ data.address }/>
<AddressLink hash={ data.address } ml={ 2 } fontWeight={ 700 }/>
<AddressIcon hash={ data.address } mr={ 2 }/>
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block" whiteSpace="nowrap" overflow="hidden">
<AddressLink hash={ data.address } fontWeight={ 700 } display="block" w="100%"/>
</Box>
</Address>
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Link fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
{ data.block_number }
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</Link>
</Flex>
);
......@@ -57,9 +65,9 @@ const SearchResultListItem = ({ data }: Props) => {
return (
<Flex alignItems="center" overflow="hidden">
<Icon as={ txIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Address>
<AddressLink hash={ data.tx_hash } type="transaction" fontWeight={ 700 }/>
</Address>
<chakra.mark display="block" overflow="hidden">
<AddressLink hash={ data.tx_hash } type="transaction" fontWeight={ 700 } display="block"/>
</chakra.mark>
</Flex>
);
}
......@@ -74,13 +82,17 @@ const SearchResultListItem = ({ data }: Props) => {
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<HashStringShortenDynamic hash={ data.block_hash }/>
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block" w="100%" whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.block_hash }/>
</Box>
);
}
case 'contract':
case 'address': {
return data.name ? <Text>{ data.name }</Text> : null;
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return data.name ? <span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/> : null;
}
default:
......
......@@ -5,6 +5,7 @@ import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -14,22 +15,22 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
searchTerm: string;
}
const SearchResultTableItem = ({ data }: Props) => {
const SearchResultTableItem = ({ data, searchTerm }: Props) => {
const content = (() => {
switch (data.type) {
case 'token': {
const name = data.name + (data.symbol ? ` (${ data.symbol })` : '');
return (
<>
<Td fontSize="sm">
<Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name }/>
<Link ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 }>
<span>
{ data.name }{ data.symbol ? ` (${ data.symbol })` : '' }
</span>
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</Link>
</Flex>
</Td>
......@@ -45,16 +46,21 @@ const SearchResultTableItem = ({ data }: Props) => {
case 'contract':
case 'address': {
if (data.name) {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return (
<>
<Td fontSize="sm">
<Address>
<AddressIcon hash={ data.address }/>
<AddressLink fontWeight={ 700 } ml={ 2 } hash={ data.address }/>
</Address>
<Flex alignItems="center" overflow="hidden">
<AddressIcon hash={ data.address } mr={ 2 }/>
<Link href={ link('address_index', { id: data.address }) } fontWeight={ 700 } overflow="hidden" whiteSpace="nowrap">
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.address }/>
</Box>
</Link>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
{ data.name }
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/>
</Td>
</>
);
......@@ -63,26 +69,30 @@ const SearchResultTableItem = ({ data }: Props) => {
return (
<Td colSpan={ 2 } fontSize="sm">
<Address>
<AddressIcon hash={ data.address }/>
<AddressLink hash={ data.address } ml={ 2 } type="address" fontWeight={ 700 }/>
<AddressIcon hash={ data.address } mr={ 2 }/>
<mark>
<AddressLink hash={ data.address } type="address" fontWeight={ 700 }/>
</mark>
</Address>
</Td>
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<>
<Td fontSize="sm">
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Link fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
{ data.block_number }
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</Link>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<Box overflow="hidden" whiteSpace="nowrap">
<Box overflow="hidden" whiteSpace="nowrap" as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.block_hash }/>
</Box>
</Td>
......@@ -95,9 +105,9 @@ const SearchResultTableItem = ({ data }: Props) => {
<Td colSpan={ 2 } fontSize="sm">
<Flex alignItems="center">
<Icon as={ txIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Address>
<mark>
<AddressLink hash={ data.tx_hash } type="transaction" fontWeight={ 700 }/>
</Address>
</mark>
</Flex>
</Td>
);
......
......@@ -77,7 +77,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
</PopoverTrigger>
<PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }>
<PopoverBody py={ 6 }>
<SearchBarSuggest query={ query }/>
<SearchBarSuggest query={ query } searchTerm={ searchTerm }/>
</PopoverBody>
</PopoverContent>
</Popover>
......
......@@ -11,9 +11,10 @@ import SearchBarSuggestItem from './SearchBarSuggestItem';
interface Props {
query: UseQueryResult<SearchResult>;
searchTerm: string;
}
const SearchBarSuggest = ({ query }: Props) => {
const SearchBarSuggest = ({ query, searchTerm }: Props) => {
const isMobile = useIsMobile();
const content = (() => {
......@@ -31,7 +32,7 @@ const SearchBarSuggest = ({ query }: Props) => {
return (
<>
<Box fontWeight={ 500 } fontSize="sm">Found <Text fontWeight={ 700 } as="span">{ num }</Text> matching results</Box>
{ query.data.items.map((item, index) => <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile }/>) }
{ query.data.items.map((item, index) => <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm }/>) }
</>
);
})();
......
import { chakra, Text, Flex, useColorModeValue, Icon } from '@chakra-ui/react';
import { chakra, Text, Flex, useColorModeValue, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import AddressIcon from 'ui/shared/address/AddressIcon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
......@@ -13,9 +14,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
const SearchBarSuggestItem = ({ data, isMobile, searchTerm }: Props) => {
const url = (() => {
switch (data.type) {
......@@ -38,12 +40,13 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
const firstRow = (() => {
switch (data.type) {
case 'token': {
const name = data.name + (data.symbol ? ` (${ data.symbol })` : '');
return (
<>
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<Text fontWeight={ 700 } ml={ 2 } w="200px" overflow="hidden" whiteSpace="nowrap" textOverflow="ellipsis" flexShrink={ 0 }>
<span>{ data.name }</span>
{ data.symbol && <span> ({ data.symbol })</span> }
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</Text>
{ !isMobile && (
<Text overflow="hidden" whiteSpace="nowrap" ml={ 2 } variant="secondary">
......@@ -55,27 +58,29 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
}
case 'contract':
case 'address': {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return (
<>
<AddressIcon hash={ data.address } mr={ 2 }/>
<chakra.span overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block" overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
<HashStringShortenDynamic hash={ data.address } isTooltipDisabled/>
</chakra.span>
{ !isMobile && (
</Box>
{ !isMobile && data.name && (
<Text variant="secondary" ml={ 2 }>
{ data.name }
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/>
</Text>
) }
</>
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<chakra.span fontWeight={ 700 }>{ data.block_number }</chakra.span>
<Box fontWeight={ 700 } as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
{ !isMobile && (
<Text variant="secondary" overflow="hidden" whiteSpace="nowrap" ml={ 2 }>
<Text variant="secondary" overflow="hidden" whiteSpace="nowrap" ml={ 2 } as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.block_hash } isTooltipDisabled/>
</Text>
) }
......@@ -86,9 +91,9 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
return (
< >
<Icon as={ txIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<chakra.span overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
<HashStringShortenDynamic hash={ data.tx_hash } isTooltipDisabled/>
</chakra.span>
</chakra.mark>
</>
);
}
......@@ -109,17 +114,23 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<Text variant="secondary" whiteSpace="nowrap" overflow="hidden">
<Text variant="secondary" whiteSpace="nowrap" overflow="hidden" as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.block_hash } isTooltipDisabled/>
</Text>
);
}
case 'contract':
case 'address': {
if (!data.name) {
return null;
}
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return (
<Text variant="secondary" whiteSpace="nowrap" overflow="hidden">
{ data.name }
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/>
</Text>
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment