chore: register domain
This commit is contained in:
24
src/components/Badge.jsx
Normal file
24
src/components/Badge.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Badge = ({ children, variant, title }) => (
|
||||
<div
|
||||
title={title}
|
||||
className={cn(
|
||||
'px-3 py-2 bg-opacity-10 rounded-2xl justify-center items-center gap-2 flex text-xs font-semibold leading-none',
|
||||
{
|
||||
'bg-emerald-500 text-emerald-500': variant === 'success',
|
||||
'bg-amber-500 text-amber-500': variant === 'premium',
|
||||
'bg-neutral-500 text-neutral-500': variant === 'taken',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Badge.propTypes = {
|
||||
children: PropTypes.node,
|
||||
variant: PropTypes.oneOf(['success', 'premium', 'taken']),
|
||||
title: PropTypes.string,
|
||||
};
|
||||
27
src/components/Button.jsx
Normal file
27
src/components/Button.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import icon from '../assets/loading.png';
|
||||
|
||||
export const Button = ({ children, onClick, loading }) => (
|
||||
<button
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-full justify-center flex',
|
||||
'text-white text-sm font-semibold leading-none w-24 text-center',
|
||||
loading ? 'bg-blue-400' : 'bg-blue-600'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<img src={icon} alt="Loading" className="animate-spin w-4" />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
Button.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
loading: PropTypes.bool,
|
||||
};
|
||||
35
src/components/RegisterButton.jsx
Normal file
35
src/components/RegisterButton.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useRegister } from '../hooks/useRegister';
|
||||
import { domainDetails } from '../types';
|
||||
import { Button } from './Button';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const RegisterButton = ({ details }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const register = useRegister(details);
|
||||
const client = useQueryClient();
|
||||
|
||||
const onClick = () => {
|
||||
setLoading(true);
|
||||
register()
|
||||
.then(() => {
|
||||
toast.success('Domain registered');
|
||||
client.refetchQueries();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.cause?.shortMessage || e.cause?.message);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={onClick} loading={loading}>
|
||||
Register
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
RegisterButton.propTypes = {
|
||||
details: domainDetails,
|
||||
};
|
||||
78
src/components/search/Search.jsx
Normal file
78
src/components/search/Search.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '@uidotdev/usehooks';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { SearchStatus } from './SearchStatus';
|
||||
|
||||
import { SearchCTA } from './SearchCTA';
|
||||
import { SearchPrice } from './SearchPrice';
|
||||
import { DEV_MODE, TLD } from '../../constants';
|
||||
import { useDomainStatus } from '../../hooks/useDomainStatus';
|
||||
|
||||
export const Search = () => {
|
||||
// "term" is the value from the input (first part if it contains a ".")
|
||||
const [term, setTerm] = useState('');
|
||||
|
||||
// label is the same as term, but delayed 300ms (while the user is typing)
|
||||
const label = useDebounce(term, 300);
|
||||
|
||||
// data contains all the info about the domain
|
||||
const { data, failureReason } = useDomainStatus({ label });
|
||||
const error = failureReason?.cause?.shortMessage;
|
||||
|
||||
// only _show_ the second line if we have a label
|
||||
const show = !!label;
|
||||
|
||||
// if there's a "." (like "foo.bar"), we only use the first part (foo)
|
||||
const onChange = (term) => setTerm(term.split('.').at(0));
|
||||
|
||||
// ensure the current data is related to the final search term
|
||||
const safeData = term === data?.label ? data : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full bg-white rounded-2xl border border-zinc-300 shadow">
|
||||
<SearchInput expand={show} onChange={onChange} />
|
||||
{show && (
|
||||
<div className="w-full px-5 pt-3 pb-4 bg-white flex-col justify-start items-start gap-4 inline-flex rounded-b-2xl">
|
||||
<div className="self-stretch py-1 justify-between items-center inline-flex">
|
||||
<div className="rounded justify-start items-center gap-2 flex">
|
||||
<div>
|
||||
<span className="text-neutral-950 text-xl font-medium leading-loose">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-neutral-400 text-xl font-medium leading-loose">
|
||||
.{TLD}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<SearchStatus details={safeData} />
|
||||
</div>
|
||||
<div className="justify-start items-center gap-4 flex">
|
||||
<SearchPrice details={safeData} />
|
||||
<SearchCTA details={safeData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{DEV_MODE && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-red-600 font-semibold bg-yellow-300 px-4 py-2 rounded-md">
|
||||
TESTNET
|
||||
</span>
|
||||
{error && (
|
||||
<div className="p-2 text-red-600 bg-red-100 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<pre className="p-2 text-gray-500 bg-gray-100 border border-gray-500 rounded-lg">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
49
src/components/search/SearchCTA.jsx
Normal file
49
src/components/search/SearchCTA.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { useAccount } from 'wagmi';
|
||||
import { TLD } from '../../constants';
|
||||
import { domainDetails } from '../../types';
|
||||
import { RegisterButton } from '../RegisterButton';
|
||||
|
||||
export const SearchCTA = ({ details }) => {
|
||||
const { address, isConnected } = useAccount();
|
||||
|
||||
if (!details) {
|
||||
return <Skeleton className="w-24 h-7" />;
|
||||
}
|
||||
|
||||
if (isConnected && details.owner === address) {
|
||||
const url = `https://hns.id/domain/${details.label}.${TLD}`;
|
||||
return (
|
||||
<a
|
||||
className=" px-3 py-2 bg-white rounded-full border border-blue-600 justify-center items-center gap-2.5 inline-flex"
|
||||
href={url}
|
||||
>
|
||||
<div className="text-blue-600 text-sm font-semibold leading-none">
|
||||
Manage Domain
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const canRegister =
|
||||
details.publicRegistrationOpen &&
|
||||
details.priceInWei > 0n &&
|
||||
details.labelValid &&
|
||||
details.isAvailable &&
|
||||
(!details.reservedAddress || details.reservedAddress === address);
|
||||
|
||||
if (!canRegister) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
return <ConnectButton />;
|
||||
}
|
||||
|
||||
return <RegisterButton details={details} />;
|
||||
};
|
||||
|
||||
SearchCTA.propTypes = {
|
||||
details: domainDetails,
|
||||
};
|
||||
34
src/components/search/SearchInput.jsx
Normal file
34
src/components/search/SearchInput.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import searchIcon from '../../assets/search.png';
|
||||
import { SEARCH_PLACEHOLDER } from '../../constants';
|
||||
|
||||
export const SearchInput = ({ expand, onChange }) => {
|
||||
const handleChange = (e) => onChange(e.target.value);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full justify-between items-center inline-flex relative',
|
||||
expand ? 'rounded-t-2xl' : 'rounded-2xl'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className={cn(
|
||||
'grow shrink basis-0 rounded-t-2xl text-neutral-400 text-base font-medium leading-tight tracking-tight p-5 border-zinc-300 focus:outline-none',
|
||||
expand ? 'rounded-t-2xl border-b' : 'rounded-2xl'
|
||||
)}
|
||||
onChange={handleChange}
|
||||
placeholder={SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
<div className="w-5 h-5 absolute right-4">
|
||||
<img src={searchIcon} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchInput.propTypes = {
|
||||
expand: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
27
src/components/search/SearchPrice.jsx
Normal file
27
src/components/search/SearchPrice.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { domainDetails } from '../../types';
|
||||
|
||||
export const SearchPrice = ({ details }) => {
|
||||
if (!details) {
|
||||
return <Skeleton className="w-24 h-7" />;
|
||||
}
|
||||
|
||||
if (!details.isAvailable || details.priceInWei === 0n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-5 items-center gap-1">
|
||||
<span className="text-neutral-900 text-sm font-bold leading-normal">
|
||||
${details.priceInDollars}
|
||||
</span>
|
||||
<span className="text-neutral-400 text-xs font-bold leading-none">
|
||||
/ year
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchPrice.propTypes = {
|
||||
details: domainDetails,
|
||||
};
|
||||
51
src/components/search/SearchStatus.jsx
Normal file
51
src/components/search/SearchStatus.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useAccount } from 'wagmi';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { Badge } from '../Badge';
|
||||
import { domainDetails } from '../../types';
|
||||
|
||||
import checkMark from '../../assets/check-mark.png';
|
||||
|
||||
export const SearchStatus = ({ details }) => {
|
||||
const { address, isConnected } = useAccount();
|
||||
|
||||
if (!details) {
|
||||
return <Skeleton className="w-20 h-7" />;
|
||||
}
|
||||
|
||||
if (isConnected && details.owner === address) {
|
||||
return <img src={checkMark} className="w-6" />;
|
||||
}
|
||||
|
||||
if (!details.isAvailable) {
|
||||
return <Badge variant="taken">Taken</Badge>;
|
||||
}
|
||||
|
||||
if (!details.labelValid) {
|
||||
return <Badge variant="taken">Invalid Domain</Badge>;
|
||||
}
|
||||
|
||||
if (!details.publicRegistrationOpen) {
|
||||
return <Badge variant="taken">Coming Soon</Badge>;
|
||||
}
|
||||
|
||||
if (details.reservedAddress && details.reservedAddress !== address) {
|
||||
return (
|
||||
<Badge
|
||||
variant="premium"
|
||||
title={`Reserved for: ${details.reservedAddress}`}
|
||||
>
|
||||
Reserved
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (details.isPremium) {
|
||||
return <Badge variant="premium">Premium</Badge>;
|
||||
}
|
||||
|
||||
return <Badge variant="success">Available</Badge>;
|
||||
};
|
||||
|
||||
SearchStatus.propTypes = {
|
||||
details: domainDetails,
|
||||
};
|
||||
Reference in New Issue
Block a user