mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
domains lookup page: main layout
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
) }
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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);
|
||||
@@ -0,0 +1,59 @@
|
||||
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);
|
||||
@@ -0,0 +1,60 @@
|
||||
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);
|
||||
@@ -0,0 +1,59 @@
|
||||
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);
|
||||
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -1,12 +1,107 @@
|
||||
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 }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user