Commit 825aac59 authored by tom's avatar tom

skeletons for search results

parent 4b7cec98
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
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 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';
import SearchResultListItem from 'ui/searchResults/SearchResultListItem';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
import ActionBar from 'ui/shared/ActionBar';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
import Thead from 'ui/shared/TheadSticky';
import Header from 'ui/snippets/header/Header';
import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput';
import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
......@@ -20,7 +19,8 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => {
const router = useRouter();
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(() => {
if (redirectCheckQuery.data?.redirect && redirectCheckQuery.data.parameter) {
......@@ -39,7 +39,9 @@ const SearchResultsPageContent = () => {
}
}
}
}, [ redirectCheckQuery.data, router ]);
!redirectCheckQuery.isLoading && setShowContent(true);
}, [ redirectCheckQuery, router ]);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
......@@ -50,23 +52,21 @@ const SearchResultsPageContent = () => {
return <DataFetchAlert/>;
}
if (isLoading || redirectCheckQuery.isLoading) {
return (
<Box>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable display={{ base: 'none', lg: 'block' }} columns={ [ '50%', '50%', '150px' ] }/>
</Box>
);
}
if (data.items.length === 0) {
if (!data?.items.length) {
return null;
}
return (
<>
<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>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="md" fontWeight={ 500 }>
......@@ -78,7 +78,14 @@ const SearchResultsPageContent = () => {
</Tr>
</Thead>
<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>
</Table>
</Hide>
......@@ -91,16 +98,16 @@ const SearchResultsPageContent = () => {
return null;
}
const text = isLoading || redirectCheckQuery.isLoading ? (
const text = isPlaceholderData && pagination.page === 1 ? (
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ isPaginationVisible ? 0 : 6 }/>
) : (
(
<Box mb={ isPaginationVisible ? 0 : 6 } lineHeight="32px">
<span>Found </span>
<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>
<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>
</Box>
)
......@@ -148,14 +155,17 @@ const SearchResultsPageContent = () => {
return <Header renderSearchBar={ renderSearchBar }/>;
}, [ renderSearchBar ]);
return (
<Page renderHeader={ renderHeader }>
{ isLoading || redirectCheckQuery.isLoading ?
<Skeleton h={ 10 } mb={ 6 } w="100%" maxW="222px"/> :
<PageTitle title="Search results"/>
}
const pageContent = !showContent ? <ContentLoader/> : (
<>
<PageTitle title="Search results"/>
{ bar }
{ content }
</>
);
return (
<Page renderHeader={ renderHeader }>
{ pageContent }
</Page>
);
};
......
......@@ -251,7 +251,15 @@ const TokenPageContent = () => {
isLoading={ tokenQuery.isPlaceholderData }
backLink={ backLink }
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={
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 React from 'react';
......@@ -19,9 +19,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
searchTerm: string;
isLoading?: boolean;
}
const SearchResultListItem = ({ data, searchTerm }: Props) => {
const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
const firstRow = (() => {
switch (data.type) {
......@@ -30,9 +31,15 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return (
<Flex alignItems="flex-start">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all">
<chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
<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"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal>
</Flex>
);
......@@ -80,7 +87,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
switch (data.type) {
case 'token': {
return (
<HashStringShortenDynamic hash={ data.address }/>
<Skeleton isLoaded={ !isLoading }>
<HashStringShortenDynamic hash={ data.address }/>
</Skeleton>
);
}
case 'block': {
......@@ -106,7 +115,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
<ListItemMobile py={ 3 } fontSize="sm" rowGap={ 2 }>
<Flex justifyContent="space-between" w="100%" overflow="hidden" lineHeight={ 6 }>
{ 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>
{ secondRow }
</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 React from 'react';
......@@ -18,9 +18,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
searchTerm: string;
isLoading?: boolean;
}
const SearchResultTableItem = ({ data, searchTerm }: Props) => {
const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
const content = (() => {
switch (data.type) {
......@@ -30,16 +31,22 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<>
<Td fontSize="sm">
<Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all">
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
<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"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<Box whiteSpace="nowrap" overflow="hidden">
<Skeleton isLoaded={ !isLoading } whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.address }/>
</Box>
</Skeleton>
</Td>
</>
);
......@@ -126,9 +133,9 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Tr>
{ content }
<Td fontSize="sm" textTransform="capitalize" verticalAlign="middle">
<Text variant="secondary">
{ data.type }
</Text>
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
<span>{ data.type }</span>
</Skeleton>
</Td>
</Tr>
);
......
......@@ -39,7 +39,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
{ !isLoading && !address.is_contract && appConfig.isAccountSupported && (
<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 }/> }
</Flex>
);
......
import type { LinkProps } from '@chakra-ui/react';
import type { LinkProps, FlexProps } from '@chakra-ui/react';
import { Flex, Link } from '@chakra-ui/react';
import type { LinkProps as NextLinkProps } from 'next/link';
import NextLink from 'next/link';
......@@ -6,9 +6,9 @@ import type { LegacyRef } from 'react';
import React from 'react';
// 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>) => {
if (props.isLoading) {
return <Flex alignItems="center">{ props.children }</Flex>;
const LinkInternal = ({ isLoading, ...props }: LinkProps & { isLoading?: boolean }, ref: LegacyRef<HTMLAnchorElement>) => {
if (isLoading) {
return <Flex alignItems="center" { ...props as FlexProps }>{ props.children }</Flex>;
}
if (!props.href) {
......
......@@ -59,12 +59,12 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
columnGap={ 3 }
alignItems="center"
>
<Box>
<Box h={{ base: 'auto', lg: isLoading ? 10 : 'auto' }}>
{ backLink && <BackLink { ...backLink } isLoading={ isLoading }/> }
{ beforeTitle }
<Skeleton
isLoaded={ !isLoading }
display={ isLoading ? 'inline-block' : 'inline' }
display={{ base: 'inline', lg: isLoading ? 'inline-block' : 'inline' }}
verticalAlign={ isLoading ? 'super' : undefined }
>
<Heading
......
......@@ -6,6 +6,8 @@ import useDebounce from 'lib/hooks/useDebounce';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
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) {
const router = useRouter();
......@@ -20,7 +22,12 @@ export default function useSearchQuery(isSearchPage = false) {
const query = useQueryWithPages({
resourceName: 'search',
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', {
......
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