Commit d808658f authored by tom's avatar tom

domains lookup page: main layout

parent 9477b5f6
......@@ -636,7 +636,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domain_events';
'domain_events' | 'domains_lookup';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......
......@@ -10,6 +10,7 @@ import appsIcon from 'icons/apps.svg';
import withdrawalsIcon from 'icons/arrows/north-east.svg';
import depositsIcon from 'icons/arrows/south-east.svg';
import blocksIcon from 'icons/block.svg';
import ensIcon from 'icons/ENS.svg';
import gearIcon from 'icons/gear.svg';
import globeIcon from 'icons/globe-b.svg';
import graphQLIcon from 'icons/graphQL.svg';
......@@ -71,6 +72,12 @@ export default function useNavItems(): ReturnType {
const verifiedContracts =
// eslint-disable-next-line max-len
{ text: 'Verified contracts', nextRoute: { pathname: '/verified-contracts' as const }, icon: verifiedIcon, isActive: pathname === '/verified-contracts' };
const ensLookup = config.features.nameService.isEnabled ? {
text: 'ENS lookup',
nextRoute: { pathname: '/name-domains' as const },
icon: ensIcon,
isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]',
} : null;
if (config.features.zkEvmRollup.isEnabled) {
blockchainNavItems = [
......@@ -83,6 +90,7 @@ export default function useNavItems(): ReturnType {
[
topAccounts,
verifiedContracts,
ensLookup,
].filter(Boolean),
];
} else if (config.features.optimisticRollup.isEnabled) {
......@@ -104,6 +112,7 @@ export default function useNavItems(): ReturnType {
[
topAccounts,
verifiedContracts,
ensLookup,
].filter(Boolean),
];
} else {
......@@ -112,6 +121,7 @@ export default function useNavItems(): ReturnType {
blocks,
topAccounts,
verifiedContracts,
ensLookup,
config.features.beaconChain.isEnabled && {
text: 'Withdrawals',
nextRoute: { pathname: '/withdrawals' as const },
......
import { chakra, Grid, Skeleton, Tooltip, Flex, Icon as ChakraIcon } from '@chakra-ui/react';
import { Grid, Skeleton, Tooltip, Flex, Icon as ChakraIcon } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -14,6 +14,8 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import NameDomainExpiryStatus from './NameDomainExpiryStatus';
interface Props {
query: UseQueryResult<EnsDomainDetailed, ResourceError<unknown>>;
}
......@@ -24,23 +26,6 @@ const NameDomainDetails = ({ query }: Props) => {
const otherAddresses = Object.entries(query.data?.otherAddresses ?? {});
const hasExpired = query.data?.expiryDate && dayjs(query.data.expiryDate).isBefore(dayjs());
const expiryText = (() => {
if (!query.data?.expiryDate) {
return null;
}
if (hasExpired) {
return <chakra.span color="red.600">Expired</chakra.span>;
}
const diff = dayjs(query.data.expiryDate).diff(dayjs(), 'day');
if (diff < 30) {
return <chakra.span color="red.600">{ diff } days left</chakra.span>;
}
return <chakra.span color="text_secondary">Expires { dayjs(query.data.expiryDate).fromNow() }</chakra.span>;
})();
return (
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}>
{ query.data?.expiryDate && (
......@@ -67,7 +52,7 @@ const NameDomainDetails = ({ query }: Props) => {
</Skeleton>
<TextSeparator color="gray.500"/>
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline">
<chakra.span>{ expiryText }</chakra.span>
<NameDomainExpiryStatus date={ query.data?.expiryDate }/>
</Skeleton>
</DetailsInfoItem>
) }
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import dayjs from 'lib/date/dayjs';
interface Props {
date: string | undefined;
}
const NameDomainExpiryStatus = ({ date }: Props) => {
if (!date) {
return null;
}
const hasExpired = dayjs(date).isBefore(dayjs());
if (hasExpired) {
return <chakra.span color="red.600">Expired</chakra.span>;
}
const diff = dayjs(date).diff(dayjs(), 'day');
if (diff < 30) {
return <chakra.span color="red.600">{ diff } days left</chakra.span>;
}
return <chakra.span color="text_secondary">Expires { dayjs(date).fromNow() }</chakra.span>;
};
export default React.memo(NameDomainExpiryStatus);
import { HStack } from '@chakra-ui/react';
import React from 'react';
import type { PaginationParams } from 'ui/shared/pagination/types';
import ActionBar from 'ui/shared/ActionBar';
import FilterInput from 'ui/shared/filters/FilterInput';
import Pagination from 'ui/shared/pagination/Pagination';
const pagination: PaginationParams = {
isVisible: true,
isLoading: false,
page: 1,
hasPages: true,
hasNextPage: true,
canGoBackwards: false,
onNextPageClick: () => {},
onPrevPageClick: () => {},
resetPage: () => {},
};
interface Props {
pagination?: PaginationParams;
searchTerm: string | undefined;
onSearchChange: (value: string) => void;
inTabsSlot?: boolean;
}
const NameDomainsActionBar = ({ searchTerm, onSearchChange }: Props) => {
const searchInput = (
<FilterInput
w={{ base: '100%', lg: '360px' }}
size="xs"
onChange={ onSearchChange }
placeholder="Search by name"
initialValue={ searchTerm }
/>
);
return (
<>
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
{ searchInput }
</HStack>
<ActionBar
mt={ -6 }
display={{ base: pagination.isVisible ? 'flex' : 'none', lg: 'flex' }}
>
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
{ searchInput }
</HStack>
<Pagination { ...pagination } ml="auto"/>
</ActionBar>
</>
);
};
export default React.memo(NameDomainsActionBar);
import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { EnsDomain } from 'types/api/ens';
import dayjs from 'lib/date/dayjs';
import NameDomainExpiryStatus from 'ui/nameDomain/NameDomainExpiryStatus';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
interface Props extends EnsDomain {
isLoading: boolean;
}
const NameDomainsListItem = ({ name, isLoading, resolvedAddress, registrationDate, expiryDate }: Props) => {
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>Domain</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<EnsEntity name={ name } isLoading={ isLoading } fontWeight={ 500 }/>
</ListItemMobileGrid.Value>
{ resolvedAddress && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity address={ resolvedAddress } isLoading={ isLoading } fontWeight={ 500 }/>
</ListItemMobileGrid.Value>
</>
) }
{ registrationDate && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Registered on</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading }>
{ dayjs(registrationDate).format('llll') }
<chakra.span color="text_secondary"> { dayjs(registrationDate).fromNow() }</chakra.span>
</Skeleton>
</ListItemMobileGrid.Value>
</>
) }
{ expiryDate && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Registered on</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">
<span>{ dayjs(expiryDate).format('llll') } </span>
<NameDomainExpiryStatus date={ expiryDate }/>
</Skeleton>
</ListItemMobileGrid.Value>
</>
) }
</ListItemMobileGrid.Container>
);
};
export default React.memo(NameDomainsListItem);
import { Table, Tbody, Tr, Th, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { EnsDomainLookupResponse } from 'types/api/ens';
import arrowIcon from 'icons/arrows/east.svg';
import { default as Thead } from 'ui/shared/TheadSticky';
import NameDomainsTableItem from './NameDomainsTableItem';
import { sortFn, type Sort } from './utils';
interface Props {
data: EnsDomainLookupResponse | undefined;
isLoading?: boolean;
sort: Sort | undefined;
onSortToggle: (event: React.MouseEvent) => void;
}
const NameDomainsTable = ({ data, isLoading, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="25%">Domain</Th>
<Th width="25%">Address</Th>
<Th width="25%" pl={ 9 }>
<Link display="flex" alignItems="center" justifyContent="flex-start" position="relative" data-field="registration_date" onClick={ onSortToggle }>
{ sort?.includes('registration_date') && (
<Icon
as={ arrowIcon }
boxSize={ 4 }
transform={ sortIconTransform }
color="link"
position="absolute"
left={ -5 }
top={ 0 }
/>
) }
<span>Registered on</span>
</Link>
</Th>
<Th width="25%">Expiration date</Th>
</Tr>
</Thead>
<Tbody>
{
data?.items
.slice()
.sort(sortFn(sort))
.map((item, index) => <NameDomainsTableItem key={ index } { ...item } isLoading={ isLoading }/>)
}
</Tbody>
</Table>
);
};
export default React.memo(NameDomainsTable);
import { chakra, Tr, Td, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { EnsDomain } from 'types/api/ens';
import dayjs from 'lib/date/dayjs';
import NameDomainExpiryStatus from 'ui/nameDomain/NameDomainExpiryStatus';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
type Props = EnsDomain & {
isLoading?: boolean;
}
const NameDomainsTableItem = ({ isLoading, name, resolvedAddress, registrationDate, expiryDate }: Props) => {
return (
<Tr>
<Td verticalAlign="middle">
<EnsEntity name={ name } isLoading={ isLoading } fontWeight={ 600 }/>
</Td>
<Td verticalAlign="middle">
{ resolvedAddress && <AddressEntity address={ resolvedAddress } isLoading={ isLoading } fontWeight={ 500 }/> }
</Td>
<Td verticalAlign="middle" pl={ 9 }>
{ registrationDate && (
<Skeleton isLoaded={ !isLoading }>
{ dayjs(registrationDate).format('llll') }
<chakra.span color="text_secondary"> { dayjs(registrationDate).fromNow() }</chakra.span>
</Skeleton>
) }
</Td>
<Td verticalAlign="middle">
{ expiryDate && (
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">
<span>{ dayjs(expiryDate).format('llll') } </span>
<NameDomainExpiryStatus date={ expiryDate }/>
</Skeleton>
) }
</Td>
</Tr>
);
};
export default React.memo(NameDomainsTableItem);
import type { EnsDomain } from 'types/api/ens';
import getNextSortValueShared from 'ui/shared/sort/getNextSortValue';
export type SortField = 'registration_date';
export type Sort = `${ SortField }-asc` | `${ SortField }-desc`;
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
registration_date: [ 'registration_date-desc', 'registration_date-asc', undefined ],
};
export const getNextSortValue = (getNextSortValueShared<SortField, Sort>).bind(undefined, SORT_SEQUENCE);
export const sortFn = (sort: Sort | undefined) => (a: EnsDomain, b: EnsDomain) => {
switch (sort) {
case 'registration_date-asc': {
if (!a.registrationDate) {
return 1;
}
if (!b.registrationDate) {
return -1;
}
return b.registrationDate?.localeCompare(a.registrationDate);
}
case 'registration_date-desc': {
if (!a.registrationDate) {
return -1;
}
if (!b.registrationDate) {
return 1;
}
return a.registrationDate.localeCompare(b.registrationDate);
}
default:
return 0;
}
};
import { Box, Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ENS_DOMAIN } from 'stubs/ENS';
import { generateListStub } from 'stubs/utils';
import NameDomainsActionBar from 'ui/nameDomains/NameDomainsActionBar';
import NameDomainsListItem from 'ui/nameDomains/NameDomainsListItem';
import NameDomainsTable from 'ui/nameDomains/NameDomainsTable';
import type { Sort, SortField } from 'ui/nameDomains/utils';
import { getNextSortValue } from 'ui/nameDomains/utils';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
const NameDomains = () => {
const router = useRouter();
const q = getQueryParamString(router.query.q);
const [ searchTerm, setSearchTerm ] = React.useState<string>(q ?? '');
const [ sort, setSort ] = React.useState<Sort>();
const { isError, isPlaceholderData, data } = useApiQuery('domains_lookup', {
pathParams: { chainId: config.chain.id },
fetchParams: {
method: 'POST',
body: {
name: 'pepecat🐾.eth',
onlyActive: true,
sort: 'registration_date',
order: 'ASC',
},
},
queryOptions: {
placeholderData: generateListStub<'domains_lookup'>(ENS_DOMAIN, 50, { totalRecords: 50 }),
},
});
const handleSortToggle = React.useCallback((event: React.MouseEvent) => {
if (isPlaceholderData) {
return;
}
const field = (event.currentTarget as HTMLDivElement).getAttribute('data-field') as SortField | undefined;
if (field) {
setSort(getNextSortValue(field));
}
}, [ isPlaceholderData ]);
const handleSearchTermChange = React.useCallback((value: string) => {
setSearchTerm(value);
}, []);
const hasActiveFilters = Boolean(searchTerm);
const content = (
<>
<Show below="lg" ssr={ false }>
<Box>
{ data?.items.map((item, index) => (
<NameDomainsListItem
key={ item.id + (isPlaceholderData ? index : '') }
{ ...item }
isLoading={ isPlaceholderData }
/>
)) }
</Box>
</Show>
<Hide below="lg" ssr={ false }>
<NameDomainsTable
data={ data }
isLoading={ isPlaceholderData }
sort={ sort }
onSortToggle={ handleSortToggle }
/>
</Hide>
</>
);
const actionBar = (
<NameDomainsActionBar
searchTerm={ searchTerm }
onSearchChange={ handleSearchTermChange }
/>
);
return (
<>
<PageTitle title="ENS domains lookup" withTextAd/>
<div>FOO BAR</div>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no name domains."
filterProps={{
emptyFilteredText: `Couldn${ apos }t find name domains that match your filter query.`,
hasActiveFilters,
}}
content={ content }
actionBar={ actionBar }
/>
</>
);
};
......
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