Developers Home»Tutorials»Create a Front-end for the Kitties Chain

Create a Front-end for the Kitties Chain

Note

If you haven't completed Part I and just want to focus on building the Kitties front-end, clone the Kitty node solution from this branch and use that to follow this part of the tutorial.

In Part I we created all of the back-end portion of our Kitties application. In this part, it's time to build a user interface which can access and interact with our custom storage items and functions. We'll be using:

In Part 2, there will only be two main sections: the first focusing on setting up the front-end template and the second focusing on building custom React components that can interact with our Kitty node.

In case you get stuck, the complete solution for this part of the tutorial can be found here.

Getting started

The first step of this tutorial is to familiarize yourself with the Substrate Front-end template. In this step we will go through an overview of what our React app will look like and the different components we'll be building. Start by installing the Front-end Template from your terminal:

git clone https://github.com/substrate-developer-hub/substrate-front-end-template.git
cd substrate-front-end-template
yarn install

Open the template in a code editor. You'll notice the following structure (we're only including the directories we care about for this tutorial):

substrate-front-end-template
|
+-- public
|   |
|   +-- assets              <-- Kitty avatar PNG files
|
+-- src                     <-- our React components
|   |
|   +-- __tests__
|   |
|   +-- config              <-- where to specify our custom types
|   |
|   +-- substrate-lib       <-- lib to give access to PolkadotJS API
|   |   |
|   |   +-- components      <-- contains TxButton, used throughout our application
|   |
|   ...
...

In a separate terminal, start an instance of node-kitties that you built in Part I (or use the Kitty node solution instead if you haven't completed part I):

# Launch `node-kitties` from its directory.
cd kitties/
./target/release/node-kitties --dev --tmp

Now, in the same directory as where you installed the Front-end template, launch it:

yarn start

You should see a tab open up with the front-end template displaying basic features of your chain. Notice that it comes with a number of prebuilt components to provide basic interactions with a Substrate Node Template blockchain.

Sketching out our components

Substrate front-end template components use Polkadot-JS API and RPC endpoints to communicate with a Substrate node. This allows us to use it to read storage items, and make extrinsics by calling our pallet's dispatchable functions. Before we get to that, let's sketch out the different parts of our application.

We'll be building out a total of 3 components:

  1. Kitties.js: this will render the Kitty pane, and contains the logics of fetching all kitties information from the connecting Substrate node.

  2. KittyCards.js: this will render a React card component containing a Kitty's relevant information, avatar and buttons to interact with it.

  3. KittyAvatar.js: this will handle the logic to map Kitty DNA to the library of PNGs we're using to create unique Kitty avatars.

Polkadot JS API Basics

Before moving on to the next section, we reccommend you read a little Polkadot JS API documentation to understand the basics of how we will be querying storage and triggering transactions. Here are some good resources:

Creating custom components

Create Kitties.js

This is the component that will get rendered by Apps.js, the top-most level component. So it does the heavy lifting, with the help of KittyAvatar.js and KittCards.js.

  1. Start by creating a file called Kitties.js in src and paste the following imports:

    import React, { useEffect, useState } from 'react'
    import { Form, Grid } from 'semantic-ui-react'
    
    import { useSubstrateState } from './substrate-lib'
    import { TxButton } from './substrate-lib/components'
    
    import KittyCards from './KittyCards'
    

    The way our custom components make use of Polkadot-JS API is by using substrate-lib, which is a wrapper around Polkadot JS API instance and allows us to retrieve account keys from the Polkadot-JS keyring. This is why we use useSubstrateState which is exported by src/substrate-lib/SubstrateContext.js and used to create the wrapper.

  2. Proceed by pasting in the following code snippet:

    const parseKitty = ({ dna, price, gender, owner }) => ({
      dna,
      price: price.toJSON(),
      gender: gender.toJSON(),
      owner: owner.toJSON(),
    })
    
    export default function Kitties(props) {
      const { api, keyring } = useSubstrateState()
      const [kittyIds, setKittyIds] = useState([])
      const [kitties, setKitties] = useState([])
      const [status, setStatus] = useState('')
      // snip
    

    The above code handles a few important things for our application:

    • parseKitty is a function to hold all kitty data and return an object.
    • Kitties enables us to subscribe to chain storage item changes and use the useEffect React hook to update the state of our other components.

    There are two things our app needs to subscribe to: storage changes in the amount of Kitties and changes in Kitty objects. To do this we'll create a subscription function for each.

    We'll use api.query.substrateKitties.countForKitties to listen for a change in the amount of Kitties, which will query CountForKitties from our Kitties pallet storage item. Then, we'll use the api.query.substrateKitties.kitties method from Polkadot-JS API to get all kitty data and transform them with the parseKitty() function.

  3. To enable this, paste the following snippet:

    // Subscription function for kitty count
    const subscribeCount = () => {
      let unsub = null
    
      const asyncFetch = async () => {
        unsub = await api.query.substrateKitties.countForKitties(async count => {
          // Fetch all kitty keys
          const entries = await api.query.substrateKitties.kitties.entries()
          const ids = entries.map(entry => entry[1].unwrap().dna)
          setKittyIds(ids)
        })
      }
    
      asyncFetch()
    
      return () => {
        unsub && unsub()
      }
    }
    
  4. Similarly for subscribeKitties, paste the following code snippet:

    // Subscription function to construct all kitty objects
    const subscribeKitties = () => {
      let unsub = null
    
      const asyncFetch = async () => {
        unsub = await api.query.substrateKitties.kitties.multi(
          kittyIds,
          kitties => {
            const kittiesMap = kitties.map(kitty => parseKitty(kitty.unwrap()))
            setKitties(kittiesMap)
          }
        )
      }
    
      asyncFetch()
    
      return () => {
        unsub && unsub()
      }
    }
    

    kitties is a storage map, Substrate supports returning the whole map of objects and we use Polkadot-JS API to iterate through the map. For each dataset, the parseKitty() function is called to return a kitty object.

    Lastly, notice the clean-up function is being returned:

    // return the unsubscription cleanup function
    return () => {
      unsub && unsub();
    };
    

    In asyncFetch we have subscribed to the Kitties storage. When the component is teared down, we want to make sure the subscription is cleaned up (unsubscribed). So we return a clean up function for the effect hook. Refer to Effects with Cleanup to learn more about cleanup functions.

  5. Now, all that's left to do in order for our component to listen for changes in our node's storage is to pass in the subscribeCount and subscribeKitties functions to React's useEffect().

    Add these lines to enable this:

    useEffect(subscribeCount, [api, keyring])
    useEffect(subscribeKitties, [api, keyring, kittyIds])
    

Congratulations! We have setup the ground work of accessing the chain and saving all Kitty information internally using React. We'll come back to the Kitties.js component later once we create all the missing components of our application to complete it.

Create KittyAvatar.js

In this component, all we're doing is mapping a library of PNG images to the bytes of our Kitty DNA. Since it's mostly all Javascript, we won't be going into much detail.

  1. Create a file called KittyAvatar.js in the src folder of your project and paste in the following code:

    import React from 'react'
    
    // Generate an array [start, start + 1, ..., end] inclusively
    const genArray = (start, end) =>
      Array.from(Array(end - start + 1).keys()).map(v => v + start)
    
    const IMAGES = {
      accessory: genArray(1, 20).map(
        n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/accessorie_${n}.png`
      ),
      body: genArray(1, 15).map(
        n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/body_${n}.png`
      ),
      eyes: genArray(1, 15).map(
        n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/eyes_${n}.png`
      ),
      mouth: genArray(1, 10).map(
        n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/mouth_${n}.png`
      ),
      fur: genArray(1, 10).map(
        n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/fur_${n}.png`
      ),
    }
    
    const dnaToAttributes = dna => {
      const attribute = (index, type) =>
        IMAGES[type][dna[index] % IMAGES[type].length]
    
      return {
        body: attribute(0, 'body'),
        eyes: attribute(1, 'eyes'),
        accessory: attribute(2, 'accessory'),
        fur: attribute(3, 'fur'),
        mouth: attribute(4, 'mouth'),
      }
    }
    
    const KittyAvatar = props => {
      const outerStyle = { height: '160px', position: 'relative', width: '50%' }
      const innerStyle = {
        height: '150px',
        position: 'absolute',
        top: '3%',
        left: '50%',
      }
      const { dna } = props
    
      if (!dna) return null
    
      const cat = dnaToAttributes(dna)
      return (
        <div style={outerStyle}>
          <img alt="body" src={cat.body} style={innerStyle} />
          <img alt="fur" src={cat.fur} style={innerStyle} />
          <img alt="mouth" src={cat.mouth} style={innerStyle} />
          <img alt="eyes" src={cat.eyes} style={innerStyle} />
          <img alt="accessory" src={cat.accessory} style={innerStyle} />
        </div>
      )
    }
    
    export default KittyAvatar
    

    Notice that the only property being passed in is dna, which will be passed from KittyCards.js.

  2. The logic in this component is based on a specific Cat Avatar library of PNGs. Download it and paste the contents of avatars/cat inside a new folder called KittyAvatar under your project's public/assets folder.

  3. Save and close KittyAvatar.js.

Write TransferModal in KittyCards.js

Our KittyCards.js component will have three sections to it:

  • TransferModal, SetPrice and BuyKitty: each of which are modal dialogs that use the TxButton component to transfer a kitty, set a kitty price, and to buy a kitty.

  • KittyCard: a card that renders the Kitty avatar using the KittyAvatar component as well as all other Kitty information (id, dna, owner, gender and price).

  • KittyCards: a component that renders a grid for KittyCard (yes, singular!) described above.

  1. As a preliminary step, create a new file called KittyCards.js and add the following imports:

    import React from 'react'
    import {
      Button,
      Card,
      Grid,
      Message,
      Modal,
      Form,
      Label,
    } from 'semantic-ui-react'
    
    import KittyAvatar from './KittyAvatar'
    import { useSubstrateState } from './substrate-lib'
    import { TxButton } from './substrate-lib/components'
    

    Let's outline what the TransferModal will do. Conveniently, the Substrate Front-end Template comes with a component called TxButton which is a useful way to include a transfer button that interacts with a node. This component will allow us to send a transaction into our node and trigger a signed extrinsic for the Kitties pallet.

    The way it is built can be broken down into the following pieces:

    • A "transfer" button exists, which opens up a modal upon being clicked.
    • This modal, we'll call "Kitty Transfer" is a Form containing (1) the Kitty ID and (2) an input field for a receiving address.
    • It also contains a "transfer" and "cancel" button.

    See the screenshot for reference:

    Kitty Transfer

    The other two modal dialogs, SetPrice and BuyKitty, follow the similar logic.

  2. The first thing we'll do is to extract the properties (or "props") we need using React hooks. These are: kitty, accountPair and setStatus. Do this by pasting in the following code snippet:

    const TransferModal = props => {
      const { kitty, accountPair, setStatus } = props;
      const [open, setOpen] = React.useState(false);
      const [formValue, setFormValue] = React.useState({});
    
      const formChange = key => (ev, el) => {
        setFormValue({ ...formValue, [key]: el.value });
      };
    

    We also need a confirmAndClose function to be passed into the TxButton component, being called when a confirmation action is triggered. This function will receive an unsubscription function from TxButton. In addition to calling this function for clean up, we will just close the modal dialog box. Paste the following snippet:

    const confirmAndClose = unsub => {
      setOpen(false)
      if (unsub && typeof unsub === 'function') unsub()
    }
    

    Our Kitty Card has a "transfer" button that opens up a modal where a user can choose an address to send their Kitty to. That modal will have:

    • a Title
    • an read-only field for a Kitty ID
    • an input field for an Account ID
    • a "Cancel" button which closes the Transfer modal
    • the TxButton React component to trigger the transaction
  3. Paste this in to complete TransferModal and read the comments to follow what each piece of code is doing:

    return (
      <Modal
        onClose={() => setOpen(false)}
        onOpen={() => setOpen(true)}
        open={open}
        trigger={
          <Button basic color="blue">
            Transfer
          </Button>
        }
      >
        <Modal.Header>Kitty Transfer</Modal.Header>
        <Modal.Content>
          <Form>
            <Form.Input fluid label="Kitty ID" readOnly value={kitty.dna} />
            <Form.Input
              fluid
              label="Receiver"
              placeholder="Receiver Address"
              onChange={formChange('target')}
            />
          </Form>
        </Modal.Content>
        <Modal.Actions>
          <Button basic color="grey" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <TxButton
            label="Transfer"
            type="SIGNED-TX"
            setStatus={setStatus}
            onClick={confirmAndClose}
            attrs={{
              palletRpc: 'substrateKitties',
              callable: 'transfer',
              inputParams: [formValue.target, kitty.dna],
              paramFields: [true, true],
            }}
          />
        </Modal.Actions>
      </Modal>
    )
    

The next part of our KittyCards.js component is to create the part that renders the KittyAvatar.js component and the data passed in from the kitties props in Kitty.js.

Write KittyCard in KittyCards.js

We'll use a Semantic UI Card component to create a card that renders the Kitty avatar as well as the Kitty DNA, gender, owner and price.

  1. As you might have guessed, we'll use React props to pass in data to our KittyCard. Paste the following code snippet:

    // Use props
    const KittyCard = props => {
      const { kitty, setStatus } = props
      const { dna = null, owner = null, gender = null, price = null } = kitty
      const displayDna = dna && dna.toJSON()
      const { currentAccount } = useSubstrateState()
      const isSelf = currentAccount.address === kitty.owner
      // snip
    
  2. Write out the contents for the Card component. Paste the following and read the comments to understand what each line is doing:

    return (
      <Card>
        {isSelf && (
          <Label as="a" floating color="teal">
            Mine
          </Label>
        )}
        {/* Render the Kitty Avatar */}
        <KittyAvatar dna={dna.toU8a()} />
        <Card.Content>
          {/* Display the Kitty DNA */}
          <Card.Meta style={{ fontSize: '.9em', overflowWrap: 'break-word' }}>
            DNA: {displayDna}
          </Card.Meta>
          {/* Display the Kitty Gender, Owner, and Price */}
          <Card.Description>
            <p style={{ overflowWrap: 'break-word' }}>Gender: {gender}</p>
            <p style={{ overflowWrap: 'break-word' }}>Owner: {owner}</p>
            <p style={{ overflowWrap: 'break-word' }}>
              Price: {price || 'Not For Sale'}
            </p>
          </Card.Description>
        </Card.Content>
        // snip...
    

    Before closing the <Card/> component we want to render the TransferModal and SetPrice components we previously built only if the Kitty is transferrable by its owner. Paste this code snippet to handle this functionality:

    <Card.Content extra style={{ textAlign: 'center' }}>
      {owner === currentAccount.address ? (
        <>
          <SetPrice kitty={kitty} setStatus={setStatus} />
          <TransferModal kitty={kitty} setStatus={setStatus} />
        </>
      ) : (
        <>
          <BuyKitty kitty={kitty} setStatus={setStatus} />
        </>
      )}
    </Card.Content>
    </Card>
    
  3. It's time to put all the pieces we've built together. We need a function to:

    • Check whether there are any Kitties to render and render a "No Kitty found here... Create one now!" message if there aren't any.
    • If there are, render them in a 3 column grid.

    Have a look at the comments to understand the parts of this code snippet and paste it in KittyCards.js:

    const KittyCards = props => {
      const { kitties, setStatus } = props
    
      if (kitties.length === 0) {
        return (
          <Message info>
            <Message.Header>
              No Kitty found here... Create one now!&nbsp;
              <span role="img" aria-label="point-down">
                👇
              </span>
            </Message.Header>
          </Message>
        )
      }
    
      return (
        <Grid columns={3}>
          {kitties.map((kitty, i) => (
            <Grid.Column key={`kitty-${i}`}>
              <KittyCard kitty={kitty} setStatus={setStatus} />
            </Grid.Column>
          ))}
        </Grid>
      )
    }
    
  4. Complete the component by adding:

    export default KittyCards
    

Note: We didn't go through the SetPrice and BuyKitty components above. Please refer to the solution code for learning how to write those.

Complete Kitties.js

Now that we've built all the bits for our front-end application, we can piece everything together.

  1. Go back to the incompleted Kitties.js file and paste this code snippet to render the KittyCard.js component inside a <Grid/>:

    return <Grid.Column width={16}>
      <h1>Kitties</h1>
      <KittyCards kitties={kitties} setStatus={setStatus}/>
    
  2. Now we'll use the <Form/> component to render our application's TxButton component. Paste in the following snippet:

        <Form style={{ margin: '1em 0' }}>
          <Form.Field style={{ textAlign: 'center' }}>
            <TxButton
              label='Create Kitty'
              type='SIGNED-TX'
              setStatus={setStatus}
              attrs={{
                palletRpc: 'substrateKitties',
                callable: 'createKitty',
                inputParams: [],
                paramFields: []
              }}
            />
          </Form.Field>
        </Form>
        <div style={{ overflowWrap: 'break-word' }}>{status}</div>
      </Grid.Column>;
    }
    

Update App.js

Now all that's left to do is connect our custom components to the main application.

  1. In App.js, import the Kitties.js component:

    import Kitties from './Kitties'
    
  2. Finally, to render Kitties.js, add a new row to <Container/>:

    <Grid.Row>
      <Kitties />
    </Grid.Row>
    

If you get stuck in any of the above section. You can refer back to the complete source code of:

🎉*Congratulations!*🎉 You have finished the Substrate Kitties front-end tutorial! In our solution, we implemented the Set Price modal for you to use as an additional example. There are lots of ways you can extend what you've learnt here, such as adding a "Breed Kitty" component, creating a marketplace for different types of avatars, or recreating this front-end with your own UI components. We encourage you to keep learning!

Next steps

Last edit: on

Run into problems?
Let us Know