Commit 825aac59 authored by tom's avatar tom

skeletons for search results

parent 4b7cec98
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import SearchResults from 'ui/pages/SearchResults';
const SearchResults = dynamic(() => import('ui/pages/SearchResults'), { ssr: false });
const SearchResultsPage: NextPage = () => { const SearchResultsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
......
import type { SearchResult, SearchResultItem } from 'types/api/search';
import { ADDRESS_HASH } from './addressParams';
export const SEARCH_RESULT_ITEM: SearchResultItem = {
address: ADDRESS_HASH,
address_url: '/address/0x3714A8C7824B22271550894f7555f0a672f97809',
name: 'USDC',
symbol: 'USDC',
token_url: '/token/0x3714A8C7824B22271550894f7555f0a672f97809',
type: 'token',
};
export const SEARCH_RESULT_NEXT_PAGE_PARAMS: SearchResult['next_page_params'] = {
address_hash: ADDRESS_HASH,
block_hash: null,
holder_count: 11,
inserted_at: '2023-05-19T17:21:19.203681Z',
item_type: 'token',
items_count: 50,
name: 'USDCTest',
q: 'usd',
tx_hash: null,
};
...@@ -6,13 +6,12 @@ import React from 'react'; ...@@ -6,13 +6,12 @@ import React from 'react';
import SearchResultListItem from 'ui/searchResults/SearchResultListItem'; import SearchResultListItem from 'ui/searchResults/SearchResultListItem';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem'; import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import Thead from 'ui/shared/TheadSticky';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput'; import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput';
import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery'; import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
...@@ -20,7 +19,8 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery'; ...@@ -20,7 +19,8 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => { const SearchResultsPageContent = () => {
const router = useRouter(); const router = useRouter();
const { query, redirectCheckQuery, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true); const { query, redirectCheckQuery, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true);
const { data, isError, isLoading, pagination, isPaginationVisible } = query; const { data, isError, isPlaceholderData, pagination, isPaginationVisible } = query;
const [ showContent, setShowContent ] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (redirectCheckQuery.data?.redirect && redirectCheckQuery.data.parameter) { if (redirectCheckQuery.data?.redirect && redirectCheckQuery.data.parameter) {
...@@ -39,7 +39,9 @@ const SearchResultsPageContent = () => { ...@@ -39,7 +39,9 @@ const SearchResultsPageContent = () => {
} }
} }
} }
}, [ redirectCheckQuery.data, router ]);
!redirectCheckQuery.isLoading && setShowContent(true);
}, [ redirectCheckQuery, router ]);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -50,23 +52,21 @@ const SearchResultsPageContent = () => { ...@@ -50,23 +52,21 @@ const SearchResultsPageContent = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading || redirectCheckQuery.isLoading) { if (!data?.items.length) {
return (
<Box>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable display={{ base: 'none', lg: 'block' }} columns={ [ '50%', '50%', '150px' ] }/>
</Box>
);
}
if (data.items.length === 0) {
return null; return null;
} }
return ( return (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map((item, index) => <SearchResultListItem key={ index } data={ item } searchTerm={ debouncedSearchTerm }/>) } { data.items.map((item, index) => (
<SearchResultListItem
key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index }
data={ item }
searchTerm={ debouncedSearchTerm }
isLoading={ isPlaceholderData }
/>
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table variant="simple" size="md" fontWeight={ 500 }> <Table variant="simple" size="md" fontWeight={ 500 }>
...@@ -78,7 +78,14 @@ const SearchResultsPageContent = () => { ...@@ -78,7 +78,14 @@ const SearchResultsPageContent = () => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.items.map((item, index) => <SearchResultTableItem key={ index } data={ item } searchTerm={ debouncedSearchTerm }/>) } { data.items.map((item, index) => (
<SearchResultTableItem
key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index }
data={ item }
searchTerm={ debouncedSearchTerm }
isLoading={ isPlaceholderData }
/>
)) }
</Tbody> </Tbody>
</Table> </Table>
</Hide> </Hide>
...@@ -91,16 +98,16 @@ const SearchResultsPageContent = () => { ...@@ -91,16 +98,16 @@ const SearchResultsPageContent = () => {
return null; return null;
} }
const text = isLoading || redirectCheckQuery.isLoading ? ( const text = isPlaceholderData && pagination.page === 1 ? (
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ isPaginationVisible ? 0 : 6 }/> <Skeleton h={ 6 } w="280px" borderRadius="full" mb={ isPaginationVisible ? 0 : 6 }/>
) : ( ) : (
( (
<Box mb={ isPaginationVisible ? 0 : 6 } lineHeight="32px"> <Box mb={ isPaginationVisible ? 0 : 6 } lineHeight="32px">
<span>Found </span> <span>Found </span>
<chakra.span fontWeight={ 700 }> <chakra.span fontWeight={ 700 }>
{ pagination.page > 1 ? 50 : data.items.length }{ data.next_page_params || pagination.page > 1 ? '+' : '' } { pagination.page > 1 ? 50 : data?.items.length }{ data?.next_page_params || pagination.page > 1 ? '+' : '' }
</chakra.span> </chakra.span>
<span> matching result{ data.items.length > 1 || pagination.page > 1 ? 's' : '' } for </span> <span> matching result{ (data?.items && data.items.length > 1) || pagination.page > 1 ? 's' : '' } for </span>
<chakra.span fontWeight={ 700 }>{ debouncedSearchTerm }</chakra.span> <chakra.span fontWeight={ 700 }>{ debouncedSearchTerm }</chakra.span>
</Box> </Box>
) )
...@@ -148,14 +155,17 @@ const SearchResultsPageContent = () => { ...@@ -148,14 +155,17 @@ const SearchResultsPageContent = () => {
return <Header renderSearchBar={ renderSearchBar }/>; return <Header renderSearchBar={ renderSearchBar }/>;
}, [ renderSearchBar ]); }, [ renderSearchBar ]);
return ( const pageContent = !showContent ? <ContentLoader/> : (
<Page renderHeader={ renderHeader }> <>
{ isLoading || redirectCheckQuery.isLoading ? <PageTitle title="Search results"/>
<Skeleton h={ 10 } mb={ 6 } w="100%" maxW="222px"/> :
<PageTitle title="Search results"/>
}
{ bar } { bar }
{ content } { content }
</>
);
return (
<Page renderHeader={ renderHeader }>
{ pageContent }
</Page> </Page>
); );
}; };
......
...@@ -251,7 +251,15 @@ const TokenPageContent = () => { ...@@ -251,7 +251,15 @@ const TokenPageContent = () => {
isLoading={ tokenQuery.isPlaceholderData } isLoading={ tokenQuery.isPlaceholderData }
backLink={ backLink } backLink={ backLink }
beforeTitle={ ( beforeTitle={ (
<TokenLogo data={ tokenQuery.data } boxSize={ 6 } isLoading={ tokenQuery.isPlaceholderData } display="inline-block" mr={ 2 }/> <TokenLogo
data={ tokenQuery.data }
boxSize={ 6 }
isLoading={ tokenQuery.isPlaceholderData }
display="inline-block"
mr={ 2 }
my={{ base: 'auto', lg: tokenQuery.isPlaceholderData ? 2 : 'auto' }}
verticalAlign={{ base: undefined, lg: tokenQuery.isPlaceholderData ? 'text-bottom' : undefined }}
/>
) } ) }
afterTitle={ afterTitle={
verifiedInfoQuery.data?.tokenAddress ? verifiedInfoQuery.data?.tokenAddress ?
......
import { Text, Flex, Icon, Box, chakra } from '@chakra-ui/react'; import { Flex, Icon, Box, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -19,9 +19,10 @@ import TokenLogo from 'ui/shared/TokenLogo'; ...@@ -19,9 +19,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: SearchResultItem; data: SearchResultItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean;
} }
const SearchResultListItem = ({ data, searchTerm }: Props) => { const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
const firstRow = (() => { const firstRow = (() => {
switch (data.type) { switch (data.type) {
...@@ -30,9 +31,15 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -30,9 +31,15 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return ( return (
<Flex alignItems="flex-start"> <Flex alignItems="flex-start">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/> <TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 } isLoading={ isLoading }/>
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all"> <LinkInternal
<chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/> ml={ 2 }
href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
); );
...@@ -80,7 +87,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -80,7 +87,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
switch (data.type) { switch (data.type) {
case 'token': { case 'token': {
return ( return (
<HashStringShortenDynamic hash={ data.address }/> <Skeleton isLoaded={ !isLoading }>
<HashStringShortenDynamic hash={ data.address }/>
</Skeleton>
); );
} }
case 'block': { case 'block': {
...@@ -106,7 +115,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -106,7 +115,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
<ListItemMobile py={ 3 } fontSize="sm" rowGap={ 2 }> <ListItemMobile py={ 3 } fontSize="sm" rowGap={ 2 }>
<Flex justifyContent="space-between" w="100%" overflow="hidden" lineHeight={ 6 }> <Flex justifyContent="space-between" w="100%" overflow="hidden" lineHeight={ 6 }>
{ firstRow } { firstRow }
<Text variant="secondary" ml={ 8 } textTransform="capitalize">{ data.type }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" ml={ 8 } textTransform="capitalize">
<span>{ data.type }</span>
</Skeleton>
</Flex> </Flex>
{ secondRow } { secondRow }
</ListItemMobile> </ListItemMobile>
......
import { Tr, Td, Text, Flex, Icon, Box } from '@chakra-ui/react'; import { Tr, Td, Flex, Icon, Box, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -18,9 +18,10 @@ import TokenLogo from 'ui/shared/TokenLogo'; ...@@ -18,9 +18,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: SearchResultItem; data: SearchResultItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean;
} }
const SearchResultTableItem = ({ data, searchTerm }: Props) => { const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
const content = (() => { const content = (() => {
switch (data.type) { switch (data.type) {
...@@ -30,16 +31,22 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => { ...@@ -30,16 +31,22 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<> <>
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center"> <Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/> <TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 } isLoading={ isLoading }/>
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all"> <LinkInternal
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/> ml={ 2 }
href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
</Td> </Td>
<Td fontSize="sm" verticalAlign="middle"> <Td fontSize="sm" verticalAlign="middle">
<Box whiteSpace="nowrap" overflow="hidden"> <Skeleton isLoaded={ !isLoading } whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.address }/> <HashStringShortenDynamic hash={ data.address }/>
</Box> </Skeleton>
</Td> </Td>
</> </>
); );
...@@ -126,9 +133,9 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => { ...@@ -126,9 +133,9 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Tr> <Tr>
{ content } { content }
<Td fontSize="sm" textTransform="capitalize" verticalAlign="middle"> <Td fontSize="sm" textTransform="capitalize" verticalAlign="middle">
<Text variant="secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
{ data.type } <span>{ data.type }</span>
</Text> </Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -39,7 +39,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props ...@@ -39,7 +39,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
{ !isLoading && !address.is_contract && appConfig.isAccountSupported && ( { !isLoading && !address.is_contract && appConfig.isAccountSupported && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/> <AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading }/> <AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/>
{ appConfig.isAccountSupported && <AddressActionsMenu isLoading={ isLoading }/> } { appConfig.isAccountSupported && <AddressActionsMenu isLoading={ isLoading }/> }
</Flex> </Flex>
); );
......
import type { LinkProps } from '@chakra-ui/react'; import type { LinkProps, FlexProps } from '@chakra-ui/react';
import { Flex, Link } from '@chakra-ui/react'; import { Flex, Link } from '@chakra-ui/react';
import type { LinkProps as NextLinkProps } from 'next/link'; import type { LinkProps as NextLinkProps } from 'next/link';
import NextLink from 'next/link'; import NextLink from 'next/link';
...@@ -6,9 +6,9 @@ import type { LegacyRef } from 'react'; ...@@ -6,9 +6,9 @@ import type { LegacyRef } from 'react';
import React from 'react'; import React from 'react';
// NOTE! use this component only for links to pages that are completely implemented in new UI // NOTE! use this component only for links to pages that are completely implemented in new UI
const LinkInternal = (props: LinkProps & { isLoading?: boolean }, ref: LegacyRef<HTMLAnchorElement>) => { const LinkInternal = ({ isLoading, ...props }: LinkProps & { isLoading?: boolean }, ref: LegacyRef<HTMLAnchorElement>) => {
if (props.isLoading) { if (isLoading) {
return <Flex alignItems="center">{ props.children }</Flex>; return <Flex alignItems="center" { ...props as FlexProps }>{ props.children }</Flex>;
} }
if (!props.href) { if (!props.href) {
......
...@@ -59,12 +59,12 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -59,12 +59,12 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
columnGap={ 3 } columnGap={ 3 }
alignItems="center" alignItems="center"
> >
<Box> <Box h={{ base: 'auto', lg: isLoading ? 10 : 'auto' }}>
{ backLink && <BackLink { ...backLink } isLoading={ isLoading }/> } { backLink && <BackLink { ...backLink } isLoading={ isLoading }/> }
{ beforeTitle } { beforeTitle }
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
display={ isLoading ? 'inline-block' : 'inline' } display={{ base: 'inline', lg: isLoading ? 'inline-block' : 'inline' }}
verticalAlign={ isLoading ? 'super' : undefined } verticalAlign={ isLoading ? 'super' : undefined }
> >
<Heading <Heading
......
...@@ -6,6 +6,8 @@ import useDebounce from 'lib/hooks/useDebounce'; ...@@ -6,6 +6,8 @@ import useDebounce from 'lib/hooks/useDebounce';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect'; import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { SEARCH_RESULT_ITEM, SEARCH_RESULT_NEXT_PAGE_PARAMS } from 'stubs/search';
import { generateListStub } from 'stubs/utils';
export default function useSearchQuery(isSearchPage = false) { export default function useSearchQuery(isSearchPage = false) {
const router = useRouter(); const router = useRouter();
...@@ -20,7 +22,12 @@ export default function useSearchQuery(isSearchPage = false) { ...@@ -20,7 +22,12 @@ export default function useSearchQuery(isSearchPage = false) {
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'search', resourceName: 'search',
filters: { q: debouncedSearchTerm }, filters: { q: debouncedSearchTerm },
options: { enabled: debouncedSearchTerm.trim().length > 0 }, options: {
enabled: debouncedSearchTerm.trim().length > 0,
placeholderData: isSearchPage ?
generateListStub<'search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }) :
undefined,
},
}); });
const redirectCheckQuery = useApiQuery('search_check_redirect', { const redirectCheckQuery = useApiQuery('search_check_redirect', {
......
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