Commit cf2149ad authored by tom's avatar tom

search results page for desktop

parent 48a0722c
...@@ -463,6 +463,7 @@ frontend: ...@@ -463,6 +463,7 @@ frontend:
- "/block" - "/block"
- "/address" - "/address"
- "/stats" - "/stats"
- "/search-results"
resources: resources:
limits: limits:
memory: memory:
......
...@@ -320,6 +320,7 @@ frontend: ...@@ -320,6 +320,7 @@ frontend:
- "/login" - "/login"
- "/address" - "/address"
- "/stats" - "/stats"
- "/search-results"
resources: resources:
limits: limits:
memory: memory:
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import SearchResults from 'ui/pages/SearchResults';
const SearchResultsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<SearchResults/>
</>
);
};
export default SearchResultsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import { Box, chakra, Table, Tbody, Tr, Th, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
// const data = {
// items: [
// {
// address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
// address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
// name: 'Toms NFT',
// symbol: 'TNT',
// token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
// type: 'token' as const,
// },
// {
// address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
// address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
// name: 'TomToken',
// symbol: 'pdE1B',
// token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
// type: 'token' as const,
// },
// {
// block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
// block_number: 8198536,
// type: 'block' as const,
// url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
// },
// {
// address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
// name: null,
// type: 'address' as const,
// url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
// },
// {
// address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
// name: 'TomToken',
// type: 'contract' as const,
// url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
// },
// {
// tx_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
// type: 'transaction' as const,
// url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
// },
// ],
// next_page_params: null,
// };
const SearchResultsPageContent = () => {
const router = useRouter();
const searchTerm = String(router.query.q || '');
const { data, isError, isLoading } = useApiQuery('search', {
queryParams: { q: searchTerm },
queryOptions: {
enabled: Boolean(searchTerm),
},
});
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<Box>
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ 6 }/>
<SkeletonTable columns={ [ '33%', '34%', '33%' ] }/>
</Box>
);
}
return (
<>
<Box mb={ 6 }>
<span>Found </span>
<chakra.span fontWeight={ 700 }>{ data.items.length }</chakra.span>
<span> matching results for </span>
<chakra.span fontWeight={ 700 }>{ searchTerm }</chakra.span>
</Box>
{ data.items.length > 0 && (
<Table variant="simple" size="md" fontWeight={ 500 }>
<Thead top={ 0 }>
<Tr>
<Th width="33%">Search Result</Th>
<Th width="34%">Hash/address</Th>
<Th width="33%">Category</Th>
</Tr>
</Thead>
<Tbody>
{ data.items.map((item, index) => <SearchResultTableItem key={ index } data={ item }/>) }
</Tbody>
</Table>
) }
</>
);
})();
return (
<Page isSearchPage>
<PageTitle text="Search results"/>
{ content }
</Page>
);
};
export default SearchResultsPageContent;
import { Tr, Td, Text, Link, Flex, Icon } 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 link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
}
const SearchResultTableItem = ({ data }: Props) => {
const content = (() => {
switch (data.type) {
case 'token': {
return (
<>
<Td fontSize="sm">
<Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name }/>
<Text fontWeight={ 700 } ml={ 2 }>
<span>{ data.name }</span>
{ data.symbol && <span> ({ data.symbol })</span> }
</Text>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<Address>
<AddressLink hash={ data.address } type="token"/>
</Address>
</Td>
</>
);
}
case 'contract':
case 'address': {
if (data.name) {
return (
<>
<Td fontSize="sm">
<Address>
<AddressIcon hash={ data.address }/>
<Text fontWeight={ 700 } ml={ 2 }>{ data.name }</Text>
</Address>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<Address>
<AddressLink hash={ data.address } type="address"/>
</Address>
</Td>
</>
);
}
return (
<Td colSpan={ 2 } fontSize="sm">
<Address>
<AddressIcon hash={ data.address }/>
<AddressLink hash={ data.address } ml={ 2 } type="address"/>
</Address>
</Td>
);
}
case 'block': {
return (
<>
<Td fontSize="sm">
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Text fontWeight={ 700 }>
{ data.block_number }
</Text>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<Link overflow="hidden" whiteSpace="nowrap" display="block" href={ link('block', { id: String(data.block_number) }) }>
<HashStringShortenDynamic hash={ data.block_hash }/>
</Link>
</Td>
</>
);
}
case 'transaction': {
return (
<Td colSpan={ 2 } fontSize="sm">
<Flex alignItems="center">
<Icon as={ txIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Address>
<AddressLink hash={ data.tx_hash } type="transaction"/>
</Address>
</Flex>
</Td>
);
}
}
})();
return (
<Tr>
{ content }
<Td fontSize="sm" textTransform="capitalize" verticalAlign="middle">{ data.type }</Td>
</Tr>
);
};
export default React.memo(SearchResultTableItem);
...@@ -15,6 +15,7 @@ interface Props { ...@@ -15,6 +15,7 @@ interface Props {
wrapChildren?: boolean; wrapChildren?: boolean;
hideMobileHeaderOnScrollDown?: boolean; hideMobileHeaderOnScrollDown?: boolean;
isHomePage?: boolean; isHomePage?: boolean;
isSearchPage?: boolean;
} }
const Page = ({ const Page = ({
...@@ -22,6 +23,7 @@ const Page = ({ ...@@ -22,6 +23,7 @@ const Page = ({
wrapChildren = true, wrapChildren = true,
hideMobileHeaderOnScrollDown, hideMobileHeaderOnScrollDown,
isHomePage, isHomePage,
isSearchPage,
}: Props) => { }: Props) => {
const fetch = useFetch(); const fetch = useFetch();
...@@ -43,7 +45,7 @@ const Page = ({ ...@@ -43,7 +45,7 @@ const Page = ({
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/> <NavigationDesktop/>
<Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}> <Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}>
<Header isHomePage={ isHomePage } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/> <Header isHomePage={ isHomePage } isSearchPage={ isSearchPage } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }> <ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren } { renderedChildren }
</ErrorBoundary> </ErrorBoundary>
......
...@@ -13,10 +13,11 @@ import ColorModeToggler from './ColorModeToggler'; ...@@ -13,10 +13,11 @@ import ColorModeToggler from './ColorModeToggler';
type Props = { type Props = {
isHomePage?: boolean; isHomePage?: boolean;
isSearchPage?: boolean;
hideOnScrollDown?: boolean; hideOnScrollDown?: boolean;
} }
const Header = ({ hideOnScrollDown, isHomePage }: Props) => { const Header = ({ hideOnScrollDown, isHomePage, isSearchPage }: Props) => {
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
...@@ -43,7 +44,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -43,7 +44,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
<NetworkLogo/> <NetworkLogo/>
<ProfileMenuMobile/> <ProfileMenuMobile/>
</Flex> </Flex>
{ !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> } { !isHomePage && <SearchBar withShadow={ !hideOnScrollDown } isSearchPage={ isSearchPage }/> }
</Box> </Box>
<Box <Box
paddingX={ 12 } paddingX={ 12 }
...@@ -61,7 +62,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -61,7 +62,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
paddingBottom="52px" paddingBottom="52px"
> >
<Box width="100%"> <Box width="100%">
<SearchBar/> <SearchBar isSearchPage={ isSearchPage }/>
</Box> </Box>
<ColorModeToggler/> <ColorModeToggler/>
<ProfileMenuDesktop/> <ProfileMenuDesktop/>
......
...@@ -12,21 +12,24 @@ import useSearchQuery from './useSearchQuery'; ...@@ -12,21 +12,24 @@ import useSearchQuery from './useSearchQuery';
type Props = { type Props = {
withShadow?: boolean; withShadow?: boolean;
isHomepage?: boolean; isHomepage?: boolean;
isSearchPage?: boolean;
} }
const SearchBar = ({ isHomepage, withShadow }: Props) => { const SearchBar = ({ isHomepage, isSearchPage, withShadow }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const inputRef = React.useRef<HTMLFormElement>(null); const inputRef = React.useRef<HTMLFormElement>(null);
const menuRef = React.useRef<HTMLDivElement>(null); const menuRef = React.useRef<HTMLDivElement>(null);
const menuWidth = React.useRef<number>(0); const menuWidth = React.useRef<number>(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { searchTerm, handleSearchTermChange, query } = useSearchQuery(); const { searchTerm, handleSearchTermChange, query } = useSearchQuery(isSearchPage);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const url = link('search_results', undefined, { q: searchTerm }); if (searchTerm) {
window.location.assign(url); const url = link('search_results', undefined, { q: searchTerm });
window.location.assign(url);
}
}, [ searchTerm ]); }, [ searchTerm ]);
const handleFocus = React.useCallback(() => { const handleFocus = React.useCallback(() => {
...@@ -55,7 +58,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => { ...@@ -55,7 +58,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
return ( return (
<Popover <Popover
isOpen={ isOpen && searchTerm.trim().length > 0 } isOpen={ isOpen && searchTerm.trim().length > 0 && !isSearchPage }
autoFocus={ false } autoFocus={ false }
onClose={ onClose } onClose={ onClose }
placement="bottom-start" placement="bottom-start"
...@@ -70,6 +73,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => { ...@@ -70,6 +73,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
onBlur={ handleBlur } onBlur={ handleBlur }
isHomepage={ isHomepage } isHomepage={ isHomepage }
withShadow={ withShadow } withShadow={ withShadow }
value={ searchTerm }
/> />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }> <PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }>
......
...@@ -14,9 +14,10 @@ interface Props { ...@@ -14,9 +14,10 @@ interface Props {
onFocus: () => void; onFocus: () => void;
isHomepage?: boolean; isHomepage?: boolean;
withShadow?: boolean; withShadow?: boolean;
value: string;
} }
const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, withShadow }: Props, ref: React.ForwardedRef<HTMLFormElement>) => { const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, withShadow, value }: Props, ref: React.ForwardedRef<HTMLFormElement>) => {
const [ isSticky, setIsSticky ] = React.useState(false); const [ isSticky, setIsSticky ] = React.useState(false);
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -86,6 +87,7 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, withS ...@@ -86,6 +87,7 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, withS
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
_focusWithin={{ _placeholder: { color: 'gray.300' } }} _focusWithin={{ _placeholder: { color: 'gray.300' } }}
color={ useColorModeValue('black', 'white') } color={ useColorModeValue('black', 'white') }
value={ value }
/> />
</InputGroup> </InputGroup>
</chakra.form> </chakra.form>
......
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -8,53 +9,57 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -8,53 +9,57 @@ import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import link from 'lib/link/link';
const data = [ // const data = [
{ // {
address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B', // address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', // address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
name: 'Toms NFT', // name: 'Toms NFT',
symbol: 'TNT', // symbol: 'TNT',
token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', // token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
type: 'token' as const, // type: 'token' as const,
}, // },
{ // {
address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', // address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', // address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
name: 'TomToken', // name: 'TomToken',
symbol: 'pdE1B', // symbol: 'pdE1B',
token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', // token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
type: 'token' as const, // type: 'token' as const,
}, // },
{ // {
address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', // address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', // address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
name: 'TomToken', // name: 'TomToken',
symbol: 'pdE1B', // symbol: 'pdE1B',
token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', // token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
type: 'token' as const, // type: 'token' as const,
}, // },
{ // {
block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', // block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
block_number: 8198536, // block_number: 8198536,
type: 'block' as const, // type: 'block' as const,
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', // url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
}, // },
{ // {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', // address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null, // name: null,
type: 'address' as const, // type: 'address' as const,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', // url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
}, // },
{ // {
tx_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', // tx_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
type: 'transaction' as const, // type: 'transaction' as const,
url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', // url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
}, // },
]; // ];
export default function useSearchQuery() { export default function useSearchQuery(isSearchPage = false) {
const [ searchTerm, setSearchTerm ] = React.useState(''); const router = useRouter();
const initialValue = isSearchPage ? String(router.query.q || '') : '';
const [ searchTerm, setSearchTerm ] = React.useState(initialValue);
const abortControllerRef = React.useRef<AbortController>(); const abortControllerRef = React.useRef<AbortController>();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
...@@ -76,9 +81,6 @@ export default function useSearchQuery() { ...@@ -76,9 +81,6 @@ export default function useSearchQuery() {
}, },
{ {
enabled: debouncedSearchTerm.trim().length > 0, enabled: debouncedSearchTerm.trim().length > 0,
initialData: {
items: data,
},
}, },
); );
...@@ -86,6 +88,13 @@ export default function useSearchQuery() { ...@@ -86,6 +88,13 @@ export default function useSearchQuery() {
setSearchTerm(event.target.value); setSearchTerm(event.target.value);
}, []); }, []);
React.useEffect(() => {
const url = link('search_results', undefined, debouncedSearchTerm ? { q: debouncedSearchTerm } : undefined);
router.push(url, undefined, { shallow: true });
// should run only when debouncedSearchTerm updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ debouncedSearchTerm ]);
return { return {
searchTerm, searchTerm,
handleSearchTermChange, handleSearchTermChange,
......
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