Commit 85f1bcb4 authored by tom's avatar tom

domain details page

parent 21e2c004
......@@ -19,6 +19,7 @@ const PAGE_PROPS = {
hash: '',
number: '',
q: '',
name: '',
};
const TestApp = ({ children }: {children: React.ReactNode}) => {
......
......@@ -15,6 +15,7 @@ const AppContext = createContext<PageProps>({
hash: '',
number: '',
q: '',
name: '',
});
export function AppContextProvider({ children, pageProps }: Props) {
......
......@@ -40,6 +40,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page',
'/404': 'Regular page',
'/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page',
// service routes, added only to make typescript happy
'/login': 'Regular page',
......
......@@ -43,6 +43,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE,
'/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE,
......
......@@ -33,11 +33,13 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/csv-export': 'export data to CSV',
'/l2-deposits': 'deposits (L1 > L2)',
'/l2-output-roots': 'output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-txn-batches': 'tx batches (L2 blocks)',
'/l2-withdrawals': 'withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
'/404': 'error - page not found',
'/name-domains': 'domains search and resolve',
'/name-domains/[name]': '%name% domain details',
// service routes, added only to make typescript happy
'/login': 'login',
......
......@@ -10,6 +10,7 @@ export type Props = {
hash: string;
number: string;
q: string;
name: string;
}
export const base: GetServerSideProps<Props> = async({ req, query }) => {
......@@ -22,6 +23,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => {
height_or_hash: query.height_or_hash?.toString() || '',
number: query.number?.toString() || '',
q: query.q?.toString() || '',
name: query.name?.toString() || '',
},
};
};
......@@ -126,6 +128,16 @@ export const suave: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const nameService: GetServerSideProps<Props> = async(context) => {
if (!config.features.nameService.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
export const accounts: GetServerSideProps<Props> = async(context) => {
if (config.UI.views.address.hiddenViews?.top_accounts) {
return {
......
......@@ -37,6 +37,8 @@ declare module "nextjs-routes" {
| StaticRoute<"/l2-txn-batches">
| StaticRoute<"/l2-withdrawals">
| StaticRoute<"/login">
| DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/name-domains/[name]" query={ props }>
<NameDomain/>
</PageNextJs>
);
};
export default Page;
export { nameService as getServerSideProps } from 'nextjs/getServerSideProps';
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const NameDomains = dynamic(() => import('ui/pages/NameDomains'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/name-domains">
<NameDomains/>
</PageNextJs>
);
};
export default Page;
export { nameService as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -29,6 +29,7 @@ const defaultAppContext = {
hash: '',
number: '',
q: '',
name: '',
},
};
......
import type { EnsDomainDetailed } from 'types/api/ens';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
export const ENS_DOMAIN: EnsDomainDetailed = {
id: '0x126d74db13895f8d3a1d362410212731d1e1d9be8add83e388385f93d84c8c84',
name: 'kitty.cat.eth',
tokenId: '0x686f4041f059de13c12563c94bd32b8edef9e4d86c931f37abb8cb69ecf25fd6',
owner: ADDRESS_PARAMS,
resolvedAddress: ADDRESS_PARAMS,
registrant: ADDRESS_PARAMS,
registrationDate: '2023-12-20T01:29:12.000Z',
expiryDate: '2099-01-02T01:29:12.000Z',
otherAddresses: {
ETH: ADDRESS_HASH,
},
};
......@@ -11,21 +11,11 @@ export interface EnsDomain {
expiryDate?: string;
}
export interface EnsDomainDetailed {
id: string;
name: string;
export interface EnsDomainDetailed extends EnsDomain {
tokenId: string;
resolvedAddress: {
hash: string;
};
owner: {
hash: string;
};
registrant: {
hash: string;
};
registrationDate?: string;
expiryDate?: string;
otherAddresses: Record<string, string>;
}
......
import { Button, chakra, Flex, Grid, Icon, Link, Popover, PopoverBody, PopoverContent, PopoverTrigger, Skeleton, useDisclosure } from '@chakra-ui/react';
import { Button, chakra, Flex, Grid, Icon, Popover, PopoverBody, PopoverContent, PopoverTrigger, Skeleton, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
......@@ -7,6 +7,7 @@ import ensIcon from 'icons/ENS.svg';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
addressHash: string;
......@@ -96,10 +97,10 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
) }
{ (ownedDomains.length > 9 || resolvedDomains.length > 9) && (
// TODO @tom2drum add href to link
<Link>
<LinkInternal>
<span> More results</span>
<chakra.span color="text_secondary"> ({ data.totalRecords })</chakra.span>
</Link>
</LinkInternal>
) }
</PopoverBody>
</PopoverContent>
......
import { chakra, Grid, Skeleton, Tooltip, Flex, Icon as ChakraIcon } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { EnsDomainDetailed } from 'types/api/ens';
import clockIcon from 'icons/clock.svg';
import iconSearch from 'icons/search.svg';
import type { ResourceError } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
interface Props {
query: UseQueryResult<EnsDomainDetailed, ResourceError<unknown>>;
}
const NameDomainDetails = ({ query }: Props) => {
const isLoading = query.isPlaceholderData;
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 && (
<DetailsInfoItem
title="Expiration date"
// eslint-disable-next-line max-len
hint="The date the name expires, upon which there is a 90 day grace period for the owner to renew. After the 90 days, the name is released to the market"
isLoading={ isLoading }
display="inline-block"
>
<Skeleton isLoaded={ !isLoading } display="inline" mr={ 2 } mt="-2px" >
<ChakraIcon as={ clockIcon } boxSize={ 5 } color="gray.500"verticalAlign="middle"/>
</Skeleton>
{ hasExpired && (
<>
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="24px">
{ dayjs(query.data.expiryDate).fromNow() }
</Skeleton>
<TextSeparator color="gray.500"/>
</>
) }
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="24px">
{ dayjs(query.data.expiryDate).format('llll') }
</Skeleton>
<TextSeparator color="gray.500"/>
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline">
<chakra.span>{ expiryText }</chakra.span>
</Skeleton>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Registrant"
hint="The account that owns the domain name and has the rights to edit its ownership and records"
isLoading={ isLoading }
columnGap={ 2 }
flexWrap="nowrap"
>
<AddressEntity
address={ query.data?.registrant }
isLoading={ isLoading }
/>
{ /* TODO @tom2drum add correct href */ }
<Tooltip label="Lookup for related domain names">
<LinkInternal flexShrink={ 0 } display="inline-flex">
<Icon as={ iconSearch } boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
</DetailsInfoItem>
<DetailsInfoItem
title="Controller"
hint="The account that owns the rights to edit the records of this domain name"
isLoading={ isLoading }
columnGap={ 2 }
flexWrap="nowrap"
>
<AddressEntity
address={ query.data?.owner }
isLoading={ isLoading }
/>
{ /* TODO @tom2drum add correct href */ }
<Tooltip label="Lookup for related domain names">
<LinkInternal flexShrink={ 0 } display="inline-flex">
<Icon as={ iconSearch } boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
</DetailsInfoItem>
<DetailsInfoItem
title="Token ID"
hint="The Token ID of this domain name NFT"
isLoading={ isLoading }
wordBreak="break-all"
whiteSpace="pre-wrap"
>
<Skeleton isLoaded={ !isLoading }>
{ query.data?.tokenId }
</Skeleton>
</DetailsInfoItem>
{ otherAddresses.length > 0 && (
<DetailsInfoItem
title="Other addresses"
hint="Other cryptocurrency addresses added to this domain name"
isLoading={ isLoading }
flexDir="column"
alignItems="flex-start"
>
{ otherAddresses.map(([ type, address ]) => (
<Flex key={ type } columnGap={ 2 } minW="0" w="100%" overflow="hidden">
<Skeleton isLoaded={ !isLoading }>{ type }</Skeleton>
<AddressEntity
address={{ hash: address }}
isLoading={ isLoading }
noLink
noIcon
/>
</Flex>
)) }
</DetailsInfoItem>
) }
</Grid>
);
};
export default React.memo(NameDomainDetails);
import { Box } from '@chakra-ui/react';
import React from 'react';
const NameDomainHistory = () => {
return <Box>History</Box>;
};
export default React.memo(NameDomainHistory);
import { Flex, Tooltip } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import iconSearch from 'icons/search.svg';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ENS_DOMAIN } from 'stubs/ENS';
import NameDomainDetails from 'ui/nameDomain/NameDomainDetails';
import NameDomainHistory from 'ui/nameDomain/NameDomainHistory';
import TextAd from 'ui/shared/ad/TextAd';
import Icon from 'ui/shared/chakra/Icon';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
const NameDomain = () => {
const isMobile = useIsMobile();
const router = useRouter();
const domainName = getQueryParamString(router.query.name);
const infoQuery = useApiQuery('domain_info', {
pathParams: { name: domainName, chainId: config.chain.id },
queryOptions: {
placeholderData: ENS_DOMAIN,
},
});
const tabs: Array<RoutedTab> = [
{ id: 'details', title: 'Details', component: <NameDomainDetails query={ infoQuery }/> },
{ id: 'history', title: 'History', component: <NameDomainHistory/> },
];
const tabIndex = useTabIndexFromQuery(tabs);
if (infoQuery.isError) {
return <DataFetchAlert/>;
}
const isLoading = infoQuery.isPlaceholderData;
const titleSecondRow = (
<Flex columnGap={ 3 } rowGap={ 3 } fontFamily="heading" fontSize="lg" fontWeight={ 500 } alignItems="center" w="100%">
<EnsEntity
name={ domainName }
isLoading={ isLoading }
noLink
maxW="300px"
/>
<AddressEntity
address={ infoQuery.data?.resolvedAddress }
isLoading={ isLoading }
truncation={ isMobile ? 'constant' : 'dynamic' }
noLink
flexShrink={ 0 }
/>
{ /* TODO @tom2drum add correct href */ }
<Tooltip label="Lookup for related domain names">
<LinkInternal flexShrink={ 0 } display="inline-flex">
<Icon as={ iconSearch } boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
</Flex>
);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle title="ENS Domain details" secondRow={ titleSecondRow }/>
{ infoQuery.isPlaceholderData ? (
<>
<TabsSkeleton tabs={ tabs } mt={ 6 }/>
{ tabs[tabIndex]?.component }
</>
) : <RoutedTabs tabs={ tabs }/> }
</>
);
};
export default NameDomain;
import React from 'react';
import PageTitle from 'ui/shared/Page/PageTitle';
const NameDomains = () => {
return (
<>
<PageTitle title="ENS domains lookup" withTextAd/>
<div>FOO BAR</div>
</>
);
};
export default NameDomains;
......@@ -12,8 +12,7 @@ import TruncatedValue from 'ui/shared/TruncatedValue';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'name'>;
const Link = chakra((props: LinkProps) => {
// TODO @tom2drum change link href
const defaultHref = route({ pathname: '/tx/[hash]', query: { hash: props.name } });
const defaultHref = route({ pathname: '/name-domains/[name]', query: { name: props.name } });
return (
<EntityBase.Link
......
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