Finish wagmi + rainbowkit basic setup

Add logo image
Start wallet control bar implementation
pull/2/head
J M Rossy 2 years ago
parent 6e2a4265c9
commit 3d0b212db9
  1. 33
      src/components/buttons/SolidButton.tsx
  2. 88
      src/components/nav/Header.tsx
  3. 176
      src/features/wallet/WalletControlBar.tsx
  4. 3
      src/images/icons/three-dots.svg
  5. 6
      src/images/icons/wallet.svg
  6. BIN
      src/images/logos/hyperlane-bridge.png
  7. 99
      src/pages/_app.tsx
  8. 9
      tailwind.config.js

@ -1,21 +1,17 @@
import { PropsWithChildren, ReactElement } from 'react';
interface ButtonProps {
size?: 'xs' | 's' | 'm' | 'l' | 'xl';
type?: 'submit' | 'reset' | 'button';
onClick?: () => void;
color?: 'white' | 'blue' | 'green' | 'red'; // defaults to blue
classes?: string;
color?: 'white' | 'blue' | 'green' | 'red' | 'gray'; // defaults to blue
bold?: boolean;
disabled?: boolean;
classes?: string;
icon?: ReactElement;
title?: string;
passThruProps?: any;
}
export function SolidButton(props: PropsWithChildren<ButtonProps>) {
export function SolidButton(
props: PropsWithChildren<ButtonProps & React.HTMLProps<HTMLButtonElement>>,
) {
const {
size,
type,
onClick,
color: _color,
@ -24,12 +20,11 @@ export function SolidButton(props: PropsWithChildren<ButtonProps>) {
icon,
disabled,
title,
passThruProps,
...passThruProps
} = props;
const color = _color ?? 'blue';
const base = 'flex items-center justify-center rounded-full transition-all duration-1000';
const sizing = sizeToClasses(size);
const base = 'flex items-center justify-center rounded-md transition-all duration-700';
let baseColors, onHover, onActive;
if (color === 'blue') {
baseColors = 'bg-blue-500 text-white';
@ -47,10 +42,14 @@ export function SolidButton(props: PropsWithChildren<ButtonProps>) {
baseColors = 'bg-white text-black';
onHover = 'hover:bg-gray-100';
onActive = 'active:bg-gray-200';
} else if (color === 'gray') {
baseColors = 'bg-gray-100 text-blue-500';
onHover = 'hover:bg-gray-200';
onActive = 'active:bg-gray-300';
}
const onDisabled = 'disabled:bg-gray-300 disabled:text-gray-500';
const weight = bold ? 'font-semibold' : '';
const allClasses = `${base} ${sizing} ${baseColors} ${onHover} ${onDisabled} ${onActive} ${weight} ${classes}`;
const allClasses = `${base} ${baseColors} ${onHover} ${onDisabled} ${onActive} ${weight} ${classes}`;
return (
<button
@ -72,11 +71,3 @@ export function SolidButton(props: PropsWithChildren<ButtonProps>) {
</button>
);
}
function sizeToClasses(size?: string) {
if (size === 'xs') return 'h-7 px-4 py-1';
if (size === 's') return 'h-7 px-4 py-1';
if (size === 'l') return 'h-10 px-5 py-1 text-lg';
if (size === 'xl') return 'w-40 h-11 px-5 py-1.5 text-xl';
return 'px-5 py-1 h-9';
}

@ -2,12 +2,8 @@ import Image from 'next/image';
import Link from 'next/link';
import useDropdownMenu from 'react-accessible-dropdown-menu-hook';
import { links } from '../../consts/links';
import BookIcon from '../../images/icons/book.svg';
import HamburgerIcon from '../../images/icons/hamburger.svg';
import HouseIcon from '../../images/icons/house.svg';
import InfoIcon from '../../images/icons/info-circle.svg';
import SearchIcon from '../../images/icons/search.svg';
import { WalletControlBar } from '../../features/wallet/WalletControlBar';
import Bridge from '../../images/logos/hyperlane-bridge.png';
import Logo from '../../images/logos/hyperlane-logo.svg';
import Name from '../../images/logos/hyperlane-name.svg';
@ -24,87 +20,11 @@ export function Header() {
<div className="flex items-center scale-90 sm:scale-100">
<Image src={Logo} width={22} alt="" />
<Image src={Name} width={110} alt="Hyperlane" className="mt-0.5 ml-2" />
<Image src={Bridge} width={104} alt="Bridge" className="mt-0.5 ml-2" />
</div>
</Link>
<nav className="hidden sm:flex sm:space-x-8 sm:items-center">
<Link href="/" className={styles.navLink}>
Home
</Link>
<a
className={styles.navLink}
target="_blank"
href={links.explorer}
rel="noopener noreferrer"
>
Explorer
</a>
<a className={styles.navLink} target="_blank" href={links.home} rel="noopener noreferrer">
About
</a>
<a className={styles.navLink} target="_blank" href={links.docs} rel="noopener noreferrer">
Docs
</a>
</nav>
<div className="relative flex item-center sm:hidden mr-2">
<button className="hover:opacity-70 transition-all" {...buttonProps}>
<Image src={HamburgerIcon} width={22} height={22} alt="..." />
</button>
</div>
<WalletControlBar />
</div>
{/* Dropdown menu, used on mobile */}
<nav className={`${styles.dropdownContainer} ${!isOpen && 'hidden'} right-0`} role="menu">
<Link href="/" {...itemProps[0]} className={styles.dropdownOption} onClick={closeDropdown}>
<DropdownItemContent icon={HouseIcon} text="Home" />
</Link>
<a
{...itemProps[1]}
onClick={closeDropdown}
className={styles.dropdownOption}
target="_blank"
href={links.explorer}
rel="noopener noreferrer"
>
<DropdownItemContent icon={SearchIcon} text="Explorer" />
</a>
<a
{...itemProps[2]}
onClick={closeDropdown}
className={styles.dropdownOption}
target="_blank"
href={links.docs}
rel="noopener noreferrer"
>
<DropdownItemContent icon={BookIcon} text="Docs" />
</a>
<a
{...itemProps[3]}
onClick={closeDropdown}
className={styles.dropdownOption}
target="_blank"
href={links.home}
rel="noopener noreferrer"
>
<DropdownItemContent icon={InfoIcon} text="About" />
</a>
</nav>
</header>
);
}
function DropdownItemContent({ icon, text }: { icon: any; text: string }) {
return (
<>
<Image src={icon} width={18} height={18} className="opacity-70 mr-2.5" alt="" />
<span>{text}</span>
</>
);
}
const styles = {
navLink:
'pt-px flex items-center tracking-wide text-gray-600 text-[0.95rem] hover:underline hover:opacity-80 active:opacity-70 decoration-2 underline-offset-[6px] transition-all',
dropdownContainer: 'dropdown-menu w-[7.5rem] mt-1 mr-px bg-gray-50 shadow-md',
dropdownOption:
'flex items-center cursor-pointer p-2 mt-1 rounded text-gray-600 hover:underline decoration-2 underline-offset-4 transition-all',
activeEnv: 'font-medium cursor-default hover:no-underline',
};

@ -0,0 +1,176 @@
import { useConnectModal } from '@rainbow-me/rainbowkit';
import Image from 'next/image';
import useDropdownMenu from 'react-accessible-dropdown-menu-hook';
import { toast } from 'react-toastify';
import { useAccount, useDisconnect, useNetwork, useSwitchNetwork } from 'wagmi';
import { SolidButton } from '../../components/buttons/SolidButton';
import { Identicon } from '../../components/icons/Identicon';
import ChevronDown from '../../images/icons/chevron-down.svg';
import Clipboard from '../../images/icons/clipboard-plus.svg';
import Cube from '../../images/icons/cube.svg';
import ThreeDots from '../../images/icons/three-dots.svg';
import Wallet from '../../images/icons/wallet.svg';
import XCircle from '../../images/icons/x-circle.svg';
import { shortenAddress } from '../../utils/addresses';
import { tryClipboardSet } from '../../utils/clipboard';
import { logger } from '../../utils/logger';
import { useIsSsr } from '../../utils/ssr';
export function WalletControlBar() {
const isSsr = useIsSsr();
if (isSsr) {
// https://github.com/wagmi-dev/wagmi/issues/542#issuecomment-1144178142
return null;
}
return (
<div className="flex justify-center items-stretch py-1.5 px-1.5 space-x-1.5 bg-white shadow-md rounded-md">
<AccountDropdown />
<OptionsDropdown />
{/* <ChainDropdown /> */}
</div>
);
}
function AccountDropdown() {
const { address, isConnected, connector } = useAccount();
const { openConnectModal } = useConnectModal();
const { disconnectAsync } = useDisconnect();
const isAccountReady = !!(address && isConnected && connector);
const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(2);
const onClickDisconnect = async () => {
setIsOpen(false);
try {
if (!disconnectAsync) throw new Error('Disconnect function is null');
await disconnectAsync();
} catch (error) {
logger.error('Error disconnecting to wallet', error);
toast.error('Could not disconnect wallet');
}
};
const onClickCopy = async () => {
setIsOpen(false);
if (!address) return;
await tryClipboardSet(address);
};
return (
<div className="relative">
{isAccountReady ? (
<button className={styles.dropdownButton} {...buttonProps}>
<Identicon address={address} size={30} />
<div className="flex flex-col mx-3 items-start">
<div className="text-xs">{connector.name}</div>
<div className="text-xs">{shortenAddress(address, true)}</div>
</div>
<Icon src={ChevronDown} size={14} />
</button>
) : (
<SolidButton
classes="py-1.5 px-2.5"
onClick={openConnectModal}
title="Choose wallet"
icon={<Image src={Wallet} alt="" width={16} height={16} />}
>
<div className="ml-1.5 text-white text-sm">Connect Wallet</div>
</SolidButton>
)}
<div className={`${styles.dropdownContainer} ${!isOpen && 'hidden'}`} role="menu">
<a {...itemProps[0]} className={styles.dropdownOption} onClick={onClickCopy}>
<Icon src={Clipboard} alt="Copy" />
<div className="ml-2">Copy Address</div>
</a>
<a {...itemProps[1]} className={styles.dropdownOption} onClick={onClickDisconnect}>
<Icon src={XCircle} alt="Logout" />
<div className="ml-2">Disconnect</div>
</a>
</div>
</div>
);
}
function OptionsDropdown() {
const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(2);
return (
<div className="relative">
<SolidButton
{...buttonProps}
color="gray"
icon={<Image src={ThreeDots} alt="" width={24} height={12} />}
classes="py-1.5 px-2.5 h-full"
title="More options"
></SolidButton>
<div className={`${styles.dropdownContainer} ${!isOpen && 'hidden'} -right-1.5`} role="menu">
<a {...itemProps[0]} className={styles.dropdownOption} onClick={() => setIsOpen(false)}>
<div>TODO</div>
</a>
<a {...itemProps[1]} className={styles.dropdownOption} onClick={() => setIsOpen(false)}>
<div>TODO</div>
</a>
</div>
</div>
);
}
function ChainDropdown() {
const { chain } = useNetwork();
const { chains, switchNetworkAsync } = useSwitchNetwork();
const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(chains.length);
const onClickChain = async (targetChainId: number) => {
setIsOpen(false);
try {
if (!switchNetworkAsync) throw new Error('Switch function is null');
await switchNetworkAsync(targetChainId);
} catch (error) {
logger.error('Error switching network', error);
toast.error('Could not switch network');
}
};
return (
<div className="relative">
<button className={styles.dropdownButton} {...buttonProps}>
<Icon src={Cube} size={16} />
<div className="mx-2">{chain?.name || 'None'}</div>
<Icon src={ChevronDown} size={14} />
</button>
<div className={`${styles.dropdownContainer} ${!isOpen && 'hidden'} right-0`} role="menu">
{chains.map((c, i) => (
<a
key={c.name + i}
{...itemProps[0]}
className={styles.dropdownOption}
onClick={() => onClickChain(c.id)}
>
<div>{c.name}</div>
</a>
))}
</div>
</div>
);
}
function Icon({ src, alt, size }: { src: any; alt?: string; size?: number }) {
return (
<div className="flex items-center">
<Image src={src} alt={alt || ''} width={size ?? 16} height={size ?? 16} />
</div>
);
}
const styles = {
dropdownButton:
'h-full flex items-center px-3 py-2.5 rounded-md text-black hover:bg-gray-100 active:bg-gray-200 transition-all duration-300',
dropdownContainer: 'dropdown-menu w-44 mt-3 bg-white',
dropdownOption: 'flex items-center cursor-pointer p-2 mt-1 rounded hover:bg-gray-100',
};

@ -0,0 +1,3 @@
<svg width="24" height="6" viewBox="0 0 24 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.42836 5.25C2.74638 5.25 2.09232 5.01295 1.61009 4.59099C1.12785 4.16903 0.856934 3.59674 0.856934 3C0.856934 2.40326 1.12785 1.83097 1.61009 1.40901C2.09232 0.987053 2.74638 0.75 3.42836 0.75C4.11035 0.75 4.7644 0.987053 5.24664 1.40901C5.72887 1.83097 5.99979 2.40326 5.99979 3C5.99979 3.59674 5.72887 4.16903 5.24664 4.59099C4.7644 5.01295 4.11035 5.25 3.42836 5.25ZM11.9998 5.25C11.3178 5.25 10.6638 5.01295 10.1815 4.59099C9.69928 4.16903 9.42836 3.59674 9.42836 3C9.42836 2.40326 9.69928 1.83097 10.1815 1.40901C10.6638 0.987053 11.3178 0.75 11.9998 0.75C12.6818 0.75 13.3358 0.987053 13.8181 1.40901C14.3003 1.83097 14.5712 2.40326 14.5712 3C14.5712 3.59674 14.3003 4.16903 13.8181 4.59099C13.3358 5.01295 12.6818 5.25 11.9998 5.25ZM20.5712 5.25C19.8892 5.25 19.2352 5.01295 18.7529 4.59099C18.2707 4.16903 17.9998 3.59674 17.9998 3C17.9998 2.40326 18.2707 1.83097 18.7529 1.40901C19.2352 0.987053 19.8892 0.75 20.5712 0.75C21.2532 0.75 21.9073 0.987053 22.3895 1.40901C22.8717 1.83097 23.1426 2.40326 23.1426 3C23.1426 3.59674 22.8717 4.16903 22.3895 4.59099C21.9073 5.01295 21.2532 5.25 20.5712 5.25Z" fill="#025AA1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#2E3338" viewBox="0 0 16 16">
<path d="M12.136.326A1.5 1.5 0 0 1 14 1.78V3h.5A1.5 1.5 0 0 1 16 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 13.5v-9a1.5 1.5 0 0 1 1.432-1.499L12.136.326zM5.562 3H13V1.78a.5.5 0 0 0-.621-.484L5.562 3zM1.5 4a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-13z"/>
</svg>
<svg width="17" height="14" viewBox="0 0 17 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33333 13.4737C2.41667 13.4737 1.63194 13.1439 0.979167 12.4842C0.326389 11.8246 0 11.0316 0 10.1053V3.36842C0 2.44211 0.326389 1.64912 0.979167 0.989474C1.63194 0.329825 2.41667 0 3.33333 0H13.3333C14.25 0 15.0347 0.329825 15.6875 0.989474C16.3403 1.64912 16.6667 2.44211 16.6667 3.36842V10.1053C16.6667 11.0316 16.3403 11.8246 15.6875 12.4842C15.0347 13.1439 14.25 13.4737 13.3333 13.4737H3.33333ZM3.33333 3.57895H13.3333C13.7361 3.57895 14.1146 3.64211 14.4687 3.76842C14.8229 3.89474 15.1389 4.07719 15.4167 4.31579V3.36842C15.4167 2.77895 15.2153 2.2807 14.8125 1.87368C14.4097 1.46667 13.9167 1.26316 13.3333 1.26316H3.33333C2.75 1.26316 2.25694 1.46667 1.85417 1.87368C1.45139 2.2807 1.25 2.77895 1.25 3.36842V4.31579C1.52778 4.07719 1.84375 3.89474 2.19792 3.76842C2.55208 3.64211 2.93056 3.57895 3.33333 3.57895ZM1.3125 6.4421L11.25 8.86316C11.3472 8.89123 11.4479 8.89474 11.5521 8.87368C11.6562 8.85263 11.7431 8.80702 11.8125 8.73684L15.1458 5.91579C14.9653 5.59298 14.7153 5.33333 14.3958 5.13684C14.0764 4.94035 13.7222 4.84211 13.3333 4.84211H3.33333C2.84722 4.84211 2.41667 4.99298 2.04167 5.29474C1.66667 5.59649 1.42361 5.97895 1.3125 6.4421Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

@ -1,42 +1,65 @@
// import {
// RainbowKitProvider,
// connectorsForWallets,
// lightTheme,
// wallet,
// } from '@rainbow-me/rainbowkit';
// import '@rainbow-me/rainbowkit/styles.css';
import { RainbowKitProvider, connectorsForWallets, lightTheme } from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css';
import {
argentWallet,
coinbaseWallet,
injectedWallet,
ledgerWallet,
metaMaskWallet,
omniWallet,
rainbowWallet,
trustWallet,
walletConnectWallet,
} from '@rainbow-me/rainbowkit/wallets';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { AppProps } from 'next/app';
import { ToastContainer, Zoom, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { WagmiConfig, configureChains, createClient as createWagmiClient } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';
// import { WagmiConfig, configureChains, createClient as createWagmiClient } from 'wagmi';
// import { publicProvider } from 'wagmi/providers/public';
import { ErrorBoundary } from '../components/errors/ErrorBoundary';
import { AppLayout } from '../components/layout/AppLayout';
import { mainnetAndTestChains } from '../consts/chains';
import { Color } from '../styles/Color';
import '../styles/fonts.css';
import '../styles/globals.css';
import { useIsSsr } from '../utils/ssr';
// const { chains, provider } = configureChains(prodAndTestChains, [publicProvider()]);
const { chains, provider } = configureChains(mainnetAndTestChains, [publicProvider()]);
// const connectors = connectorsForWallets([
// {
// groupName: 'Recommended',
// wallets: [
// wallet.metaMask({ chains }),
// wallet.walletConnect({ chains }),
// wallet.rainbow({ chains }),
// wallet.steak({ chains }),
// ],
// },
// ]);
const connectorConfig = {
appName: 'Hyperlane Token Bridge',
chains,
};
// const wagmiClient = createWagmiClient({
// autoConnect: false, // TODO
// provider,
// connectors,
// });
const connectors = connectorsForWallets([
{
groupName: 'Recommended',
wallets: [
metaMaskWallet(connectorConfig),
injectedWallet(connectorConfig),
walletConnectWallet(connectorConfig),
ledgerWallet(connectorConfig),
],
},
{
groupName: 'More',
wallets: [
coinbaseWallet(connectorConfig),
omniWallet(connectorConfig),
rainbowWallet(connectorConfig),
trustWallet(connectorConfig),
argentWallet(connectorConfig),
],
},
]);
const wagmiClient = createWagmiClient({
autoConnect: false, // TODO
provider,
connectors,
});
const reactQueryClient = new QueryClient({
defaultOptions: {
@ -56,23 +79,23 @@ export default function App({ Component, router, pageProps }: AppProps) {
return (
<ErrorBoundary>
{/* <WagmiConfig client={wagmiClient}> */}
{/* <RainbowKitProvider
<WagmiConfig client={wagmiClient}>
<RainbowKitProvider
chains={chains}
theme={lightTheme({
accentColor: Color.primaryRed,
accentColor: Color.primaryBlue,
borderRadius: 'small',
fontStack: 'system',
})}
> */}
<QueryClientProvider client={reactQueryClient}>
<AppLayout pathName={router.pathname}>
<Component {...pageProps} />
</AppLayout>
</QueryClientProvider>
<ToastContainer transition={Zoom} position={toast.POSITION.BOTTOM_RIGHT} limit={2} />
{/* </RainbowKitProvider> */}
{/* </WagmiConfig> */}
>
<QueryClientProvider client={reactQueryClient}>
<AppLayout pathName={router.pathname}>
<Component {...pageProps} />
</AppLayout>
</QueryClientProvider>
<ToastContainer transition={Zoom} position={toast.POSITION.BOTTOM_RIGHT} limit={2} />
</RainbowKitProvider>
</WagmiConfig>
</ErrorBoundary>
);
}

@ -2,7 +2,6 @@
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
@ -90,10 +89,10 @@ module.exports = {
},
borderRadius: {
none: '0',
sm: '0.2rem',
DEFAULT: '0.3rem',
md: '0.4rem',
lg: '0.5rem',
sm: '0.25rem',
DEFAULT: '0.35rem',
md: '0.45rem',
lg: '0.55rem',
full: '9999px',
},
blur: {

Loading…
Cancel
Save