Tutorials
Auction Componentry

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).