mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
domain details page
This commit is contained in:
@@ -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 {
|
||||
|
||||
Vendored
+2
@@ -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 }>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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';
|
||||
@@ -0,0 +1,19 @@
|
||||
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: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
+1
-11
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
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);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
const NameDomainHistory = () => {
|
||||
return <Box>History</Box>;
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainHistory);
|
||||
@@ -0,0 +1,89 @@
|
||||
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;
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user