Build a custom auction component
This tutorial will walk you through building a custom auction component for your Nouns Builder DAO. This is a React specific tutorial that leverages Next.js (opens in a new tab) (Next) and its App Router (opens in a new tab). The implementation of this tutorial leverages Tailwind CSS (opens in a new tab) for styling, but the code snippets provided below are unstyled. Any styling approach will work just fine.
Create a new project and install dependencies
In a new directory, use the create-wagmi
CLI tool to spin up a Next project configured with wagmi (opens in a new tab). We'll use a template preloaded with ConnectKit (opens in a new tab).
pnpm create wagmi --template next-connectkit
Follow the command line prompts. If you don't have an Alchemy/Infura API key or WalletConnect ID handy, you can always provide it manually later on.
Install the builder utils package and its peer dependencies listed below.
pnpm add @public-assembly/builder-utils
"peerDependencies": {
"date-fns": "^2.29.3",
"graphql": "^16.6.0",
"graphql-request": "^5.0.0",
"graphql-tag": "^2.12.6",
"swr": "^1.3.0",
},
Clean up your project
For the sake of this tutorial, you won't need any of the hooks or components that arrive with wagmi's template. Besides the ConnectButton
, you can delete all of the components and functions inside each of these folders. Then make sure to visit the page.tsx
component inside your app
directory and remove the unsued imports and rendering instances.
import { ConnectButton } from '../components/ConnectButton'
export function Page() {
return <ConnectButton />
}
export default Page
Configure the provider
Builder utils provides a collection of provider components used to dynamically pass contract specific data throughout your application. Begin configuring these provider components by first navigating to your app
directory and then to the providers.tsx
component.
Import the ManagerProvider
component from builder utils and nest it beneath the wallet connection provider. This component accepts your DAO's token contract address as a prop. For this tutorial we'll use Public Assembly's. You can find this address for your DAO by navigating to the 'Smart Contract' tab beneath the artwork for your DAO on Nouns Builder (opens in a new tab).
'use client'
import { ConnectKitProvider } from 'connectkit'
import * as React from 'react'
import { WagmiConfig } from 'wagmi'
import { config } from '../wagmi'
import { ManagerProvider } from '@public-assembly/builder-utils'
export function Providers({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => setMounted(true), [])
return (
<WagmiConfig config={config}>
<ConnectKitProvider>
<ManagerProvider tokenAddress="0xd2e7684cf3e2511cc3b4538bb2885dc206583076">
{mounted && children}
</ManagerProvider>
</ConnectKitProvider>
</WagmiConfig>
)
}
Create a skeleton component
In your components
directory create a component called AuctionSkeleton.tsx
. This component will exist as a layout component that dynamically switches between the current auction and historical auctions while rendering the respective interface elements.
This component leverages the useTokenExplorer
hook which returns two functions each responsible for navigating between past tokens and the current auction. Additionally, notice the use of the 'use client'
directive at the top of the component. This ensures that this component and its children exist as part of the client bundle.
'use client'
import { useTokenExplorer } from '@public-assembly/builder-utils'
export function AuctionSkeleton({ tokenId }: { tokenId: string }) {
const { incrementId, decrementId } = useTokenExplorer({ tokenId: Number(tokenId) })
return (
<>
<button onClick={decrementId}>Backward</button>
<button onClick={incrementId}>Forward</button>
</>
)
}
Additionally you'll want to grab two more of the destructured elements returned by the useTokenExplorer hook, boolean elements that will allow you to disable the navigation buttons if the user has reached the first or last (current) token.
export function AuctionSkeleton({ tokenId }: { tokenId: string }) {
const { incrementId, decrementId, isFirstToken, isLastToken } = useTokenExplorer({
tokenId: Number(tokenId),
})
return (
<>
<button disabled={isFirstToken} onClick={decrementId}>
Backward
</button>
<button disabled={isLastToken} onClick={incrementId}>
Forward
</button>
</>
)
}
Next we'll create another component called CurrentAuction.tsx
which will render all the data associated with the current auction.
This component leverages the useTokenMetadata
hook which accepts a string representing the token id to render and returns the name and artwork for that token.
Note that the tokenId
passed into useTokenMetadata
is in the form of a number, but must be convereted into a string in order to be properly formatted for the underlying API call.
import { useTokenMetadata, useTokenExplorer } from '@public-assembly/builder-utils'
export function CurrentAuction({ tokenId }: { tokenId: string }) {
const { tokenName, tokenThumbnail } = useTokenMetadata(tokenId)
return <></>
}
We'll flesh out the CurrentAuction
component a bit more by adding an img tag and rendering the artwork alongside the token name. If you're interested in using Next's native Image component, be sure to read the documentation here (opens in a new tab).
import { useTokenMetadata, useAuctionState } from '@public-assembly/builder-utils'
export function CurrentAuction({ tokenId }: { tokenId: string }) {
const { tokenName, tokenThumbnail } = useTokenMetadata(tokenId)
return (
<>
<h1>{tokenName}</h1>
<img src={tokenThumbnail} />
</>
)
}
Next we'll create a HistoricalAuction
component. Similarly to the CurrentAuction
component, this component will utilize the useTokenMetadata
hook, but instead of grabbing the tokenId from the current state of the auction, this component will accept navigatedTokenId
as a prop. This refers to the state of the component determined by the interacting user.
import { useTokenMetadata } from '@public-assembly/builder-utils'
export function HistoricalAuction({ navigatedTokenId }: { navigatedTokenId: number }) {
const { tokenName, tokenThumbnail } = useTokenMetadata(String(navigatedTokenId))
return (
<>
<h1>{tokenName}</h1>
<img src={tokenThumbnail} width={500} height={500} />
</>
)
}
Lets return to our AuctionSkeleton
component and setup conditional rendering based on whether or not the state of the token id object reflects the current auction.
We'll create an arrow function called renderContent
that checks if the isLastToken
boolean is true. If so it will return the CurrentAuction
component, if not it will render the HistoricalAuction
component. Additionally we can use the isLastToken
boolean alongisde the isFirstToken
boolean to create disabled states for our explorer buttons.
import { useTokenExplorer } from '@public-assembly/builder-utils'
import { CurrentAuction } from './CurrentAuction'
import { HistoricalAuction } from './HistoricalAuction'
export function AuctionSkeleton({ tokenId }: { tokenId: string }) {
const { navigatedTokenId, incrementId, decrementId, isFirstToken, isLastToken } =
useTokenExplorer({ tokenId: Number(tokenId) })
const renderContent = () => {
if (isLastToken) {
return <CurrentAuction tokenId={tokenId} />
} else {
return <HistoricalAuction navigatedTokenId={navigatedTokenId} />
}
}
return (
<>
{renderContent()}
<>
<button disabled={isFirstToken} onClick={decrementId}>
Backward
</button>
<button disabled={isLastToken} onClick={incrementId}>
Forward
</button>
</>
</>
)
}
Integrate a countdown
Builder utils provides a handy hook called useCountdown
that returns a string variable countdownString
indicating when an auction will end. Additionally that same hook returns a boolean value isEnded
conveying whether or not an auction is over. We can utilize this hook in another component we'll create called AuctionCountdown.tsx
.
import { useCountdown } from '@public-assembly/builder-utils'
export function AuctionCountdown({ endTime }: { endTime: number }) {
const { countdownString, isEnded } = useCountdown(endTime)
return (
<div>
{!isEnded ? (
<>
<span>Auction ends in:</span>
<span>{countdownString}</span>
</>
) : (
<span>Auction ended</span>
)}
</div>
)
}
Add bidding functionality
Create another component called Bidding.tsx
. In it we'll leverage two hooks from builder utils to dynamically create write calls to our DAO's auction contract. This component will be rendered inside our CurrentAuction
component and utilize an html form element to handle bid submissions. This component also makes use of the AuctionCountdown
we just created in the step above.
import { AuctionCountdown } from './AuctionCountdown'
import {
useAuctionState,
useManagerContext,
useMinBidAmount,
useCreateBid,
} from '@public-assembly/builder-utils'
export function Bidding() {
const { auctionState } = useAuctionState()
const { tokenAddress } = useManagerContext()
const { minBidAmount, bidAmount, updateBidAmount, isValidBid } = useMinBidAmount()
const { createBid, createBidLoading, createBidSuccess } = useCreateBid({
bidAmount: bidAmount,
})
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
createBid?.()
}
return (
<>
<>
<div>
<span>Current Bid:</span>
<span>{auctionState?.highestBid} ETH</span>
</div>
<AuctionCountdown endTime={Number(auctionState?.endTime)} />
<span>Bidder: {auctionState?.highestBidder}</span>
</>
<div>
<form onSubmit={handleSubmit}>
<input
style={{ color: 'black' }}
type="text"
pattern="[0-9.]*"
placeholder={`${minBidAmount} ETH`}
disabled={createBidLoading}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
updateBidAmount(event.target.value)
}
/>
{!createBidLoading && !createBidSuccess ? (
<button>Place Bid</button>
) : (
<>
{createBidLoading && <span>Submitting bid</span>}
{createBidSuccess && (
<a
href={`https://nouns.build/dao/${tokenAddress}`}
target="_blank"
rel="noreferrer">
Bid placed: view on nouns.build
</a>
)}
</>
)}
</form>
</div>
</>
)
}
Include settle ability
Now that we've integrated bidding functionality, we need to make sure that our users can move on to the next auction once the current one ends. For this we'll leverage the useSettle
hook which provides a write function and respective feedback states for exactly this feature. Let's utilize this hook in a new component called SettleAuction.tsx
.
import { useSettle } from '@public-assembly/builder-utils'
export function SettleAuction() {
const { settle, settleLoading } = useSettle()
return (
<button disabled={settleLoading} onClick={() => settle?.()}>
Settle auction
</button>
)
}
Utilize dynamic routing
The last step to glue everything together is to leverage dyanmic routing from Next. With dynamic routing we'll be able to create a unique link for every minted token, providing a shareable url for our users.
Begin by creating a new folder in our app
directory, called [tokenId]
. The brackets denote that this will be a dynamic route. Within that directory, create another page component, page.tsx
. Again, notice the use of the 'use client'
directive at the top of the component. This will allow us to remove that same directive from this function's children, so you can go ahead and make those adjustments in both the AuctionSkeleton
and ConnectButton
components.
'use client'
import { ConnectButton } from '../../components/ConnectButton'
import { AuctionSkeleton } from '../../components/AuctionSkeleton'
export default function Page({ params }: { params: { tokenId: string } }) {
return (
<>
<ConnectButton />
<AuctionSkeleton tokenId={params.tokenId} />
</>
)
}
This component takes a tokenId
which will be informed by our current route and passed into our AuctionSkeleton
component. Now that we've defined our dynamic route, we'll revisit a couple of our previously created components and add the appropriate changes to handle this configuration.
In our AuctionSkeleton
component we'll utilize Next's router API to navigate to the appropriate routes when exploring tokens.
import { useTokenExplorer } from '@public-assembly/builder-utils'
import { CurrentAuction } from './CurrentAuction'
import { HistoricalAuction } from './HistoricalAuction'
import { useRouter } from 'next/navigation'
export function AuctionSkeleton({ tokenId }: { tokenId: string }) {
const { navigatedTokenId, incrementId, decrementId, isFirstToken, isLastToken } =
useTokenExplorer({ tokenId: Number(tokenId) })
const router = useRouter()
const renderContent = () => {
if (isLastToken) {
return <CurrentAuction tokenId={tokenId} />
} else {
return <HistoricalAuction navigatedTokenId={navigatedTokenId} />
}
}
function incrementAndPush() {
incrementId()
router.push(`/${navigatedTokenId + 1}`)
}
function decrementAndPush() {
decrementId()
router.push(`/${navigatedTokenId - 1}`)
}
return (
<>
{renderContent()}
<>
<button disabled={isFirstToken} onClick={decrementAndPush}>
Backward
</button>
<button disabled={isLastToken} onClick={incrementAndPush}>
Forward
</button>
</>
</>
)
}
In our SettleAuction
component, we'll implement a useEffect hook in addition to Next's router API to navigate to the next token once the previous auction has been succesfully settled.
import { useSettle, useAuctionState } from '@public-assembly/builder-utils'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export function SettleAuction() {
const { settle, settleLoading, settleSuccess } = useSettle()
const { auctionState } = useAuctionState()
const router = useRouter()
useEffect(() => {
if (settleSuccess) {
router.push(`/${auctionState.tokenId + 1}`)
}
}, [settleSuccess])
return (
<button disabled={settleLoading} onClick={() => settle?.()}>
Settle auction
</button>
)
}
Lastly, we'll return to our AuctionCountdown
component and swap in our newly configured SettleAuction
component to appear when an auction ends.
import { useCountdown } from '@public-assembly/builder-utils'
import { SettleAuction } from './SettleAuction'
export function AuctionCountdown({ endTime }: { endTime: number }) {
const { countdownString, isEnded } = useCountdown(endTime)
return (
<div className="flex flex-col">
{!isEnded ? (
<>
<span>Auction ends in:</span>
<span>{countdownString}</span>
</>
) : (
<SettleAuction />
)}
</div>
)
}
That's everything! I hope you're excited to style and share your new custom auction component. If you have any comments and suggestions for this tutorial, I'd encourage you to open an issue (opens in a new tab), create a pull request, or reach out to me directly on Twitter (opens in a new tab).