# NFT frontend application We'll create the application for the NFT contract we wrote in the [previous lesson](https://hackmd.io/rvQGQWnvSwC7MaXlMNhLhw?both). ### Create-NFT page First clone the [frontend-starter](https://github.com/LouiseMedova/gear-app-starter). Install [NodeJs](https://nodejs.org/en/download/) and [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Make sure you have the latest LTS version of the NodeJs installed. Then install yarn: ``` npm install --global yarn ``` There is `.env.example` file. Create your own `.env` file and copy `.env.example` contents to your `.env`. It contains: - `REACT_APP_NODE_ADDRESS` - this variable defines which node we'll work on. - `REACT_APP_CONTRACT_ADDRESS` - the address of the contract uploaded to the chain. - `REACT_APP_IPFS_ADDRESS` and `REACT_APP_IPFS_GATEWAY_ADDRESS` are variables which we will need when we upload media files to IPFS. Upload the contract to the chain and set up the address in the `.env` file. Put `meta.txt` file to the folder `assets/meta` and `nft_state.meta.wasm` file to the `mkdir assets/wasm`. Install packages: ``` yarn install ``` and run the app ``` yarn start ``` The main file `App.tsx` is simple: ```typescript import { useApi, useAccount } from '@gear-js/react-hooks'; import { Routing } from 'pages'; import { Header, Footer, ApiLoader } from 'components'; import { withProviders } from 'hocs'; import 'App.scss'; function Component() { const { isApiReady } = useApi(); const { isAccountReady } = useAccount(); const isAppReady = isApiReady && isAccountReady; return ( <> <Header isAccountVisible={isAccountReady} /> <main>{isAppReady ? <Routing /> : <ApiLoader />}</main> <Footer /> </> ); } export const App = withProviders(Component); ``` It checks whether the application is connected to the chain: ```typescript const { isApiReady } = useApi(); ``` It checks whether the account is connected to the application through the web extension: ```typescript const { isAccountReady } = useAccount(); ``` If the `api` is ready and `account` is connected it displays the applications pages. Let's go to `pages` folder. The `index.tsx` file is also simple: ```typescript import { Route, Routes } from 'react-router-dom'; import { Home } from './home/Home'; const routes = [ { path: '/', Page: Home }, ]; export function Routing() { const getRoutes = () => routes.map(({ path, Page }) => <Route key={path} path={path} element={<Page />} /> ); return <Routes>{getRoutes()}</Routes>; } ``` Now it has only one page `Home`. Let's create a page for NFT creation. ``` mkdir src/pages/create-nft touch src/pages/create-nft/CreateNft.tsx ``` Then move the file with styles from `assets` folder to the `create-nft` folder: ``` mv src/assets/styles/CreateNft.module.scss src/pages/create-nft ``` Let's start writing the `CreateNft.tsx`: ```typescript import styles from 'CreateNft.module.scss' export function CreateNft() { return ( <div>Create NFT</div> ) } ``` We should declare this page in the `index.tsx` and also add the route: ```typescript import { Route, Routes } from 'react-router-dom'; import { CreateNft } from './create-nft/CreateNft'; import { Home } from './home/Home'; const routes = [ { path: '/', Page: Home }, { path: '/create-nft', Page: CreateNft }, ]; export function Routing() { const getRoutes = () => routes.map(({ path, Page }) => <Route key={path} path={path} element={<Page />} /> ); return <Routes>{getRoutes()}</Routes>; } ``` Let's make a link to `CreateNft` page from the `Home` page. In the `Home.tsx` file let's write: ```typescript import { Link } from "react-router-dom"; function Home() { return ( <Link to="/create-nft"> <h3>Create NFT</h3> </Link> ) } export { Home }; ``` Now let's back to the `CreateNft` page. First we create the input form that include the NFT `title`, `description` and `image`: ```typescript import { Button, FileInput, Input } from '@gear-js/ui' import styles from './CreateNft.module.scss' export function CreateNft() { return ( <> <h2 className={styles.heading}> Create NFT</h2> <div className={styles.main}> <form className={styles.from}> <div className={styles.item}> <Input label="Name" className={styles.input} required/> </div> <div className={styles.item}> <Input label="Description" className={styles.input} required/> </div> <div className={styles.item}> <FileInput label="Image" className={styles.input} required/> </div> <Button type="submit" text="Create" className={styles.button}/> </form> </div> </> ) } ``` Let's create state that will store NFT's title, description and image and add the function `handleInputChange` and `handleImageChange` that will store this state: ```typescript import { Button, FileInput, Input } from '@gear-js/ui' import { useState } from 'react' import styles from './CreateNft.module.scss' const NftInitialState = { title: "", description: "", } export function CreateNft() { const [nftForm, setNftForm] = useState(NftInitialState); const [image, setImage] = useState<File | null>(null) const { title, description } = nftForm; const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; setNftForm(prevForm => ({...prevForm , [name]: value})) } return ( <> <h2 className={styles.heading}> Create NFT</h2> <div className={styles.main}> <form className={styles.from}> <div className={styles.item}> <Input label="Name" className={styles.input} required name="title" value={title} onChange={handleInputChange}/> </div> <div className={styles.item}> <Input label="Description" className={styles.input} required name="description" value={description} onChange={handleInputChange}/> </div> <div className={styles.item}> <FileInput label="Image" className={styles.input} onChange={setImage}/> </div> <Button type="submit" text="Create" className={styles.button}/> </form> </div> </> ) } ``` Let's also add the image preview for the uploaded image: ```typescript ... export function CreateNft() { ... return ( <> <h2 className={styles.heading}> Create NFT</h2> <div className={styles.main}> <form className={styles.from}> ... <div className={styles.item}> <FileInput label="Image" className={styles.input} onChange={setImage}/> { image ? ( <div className="image-preview"> <img src={URL.createObjectURL(image)} alt="nft" style={{width: 100, height: 100}}/> </div> ): ( <p>No image set for this NFT</p> )} </div> <Button type="submit" text="Create" className={styles.button}/> </form> </div> </> ) } ``` Next we upload the image to the IPFS and send message `Mint` to the contract. Install the [IPFS Desktop App](http://docs.ipfs.tech.ipns.localhost:8080/install/ipfs-desktop/#windows). Go to `Settings` ![](https://i.imgur.com/ssgQSvY.jpg) Find `IPFS config` ![](https://i.imgur.com/nn82YO3.png) and configure the `API` of your node: ``` "API": { "HTTPHeaders": { "Access-Control-Allow-Methods": [ "PUT", "GET", "POST" ], "Access-Control-Allow-Origin": [ "*", "https://webui.ipfs.io", "http://webui.ipfs.io.ipns.localhost:8080", "http://127.0.0.1:5001" ] } }, ``` Now we are ready to upload the files from our application. Let's start writing the function. ```typescript ... import { useIPFS } from 'hooks'; ... export function CreateNft() { ... const ipfs = useIPFS(); const createNft = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); let cid; if (image) { try { cid = await ipfs.add(image as File) } catch (error) { alert(error) } } } ... } ``` Next we should send the message to the contract. But before that let's write the necessary hooks. Create file `api.ts` in the `hooks` folder: ``` touch src/hooks/api.ts ``` We'll define the hook `useNFTMetadata` and `useSendNFTMessage`: ```typescript import { useSendMessage } from '@gear-js/react-hooks'; import metaTxt from 'assets/meta/meta.txt' import { ADDRESS } from 'consts'; import { useMetadata } from "./useMetadata"; function useNFTMetadata() { return useMetadata(metaTxt) } function useSendNFTMessage() { const meta = useNFTMetadata() return useSendMessage(ADDRESS.CONTRACT_ADDRESS, meta) } export {useNFTMetadata, useSendNFTMessage} ``` Let's continue writing the `CreateNft` function. We just create the message `payload` and send the message to the countract: ```typescript ... import { useAccount } from '@gear-js/react-hooks'; import { useSendNFTMessage } from 'hooks/api'; import { useNavigate } from 'react-router-dom'; ... export function CreateNft() { ... const ipfs = useIPFS(); const { account }= useAccount(); const navigate = useNavigate(); const handleMessage = useSendNFTMessage(); const resetForm = () => { setNftForm(NftInitialState); setImage(null) } const createNft = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); let cid; if (image) { try { cid = await ipfs.add(image as File) } catch (error) { alert(error) } } const tokenMetadata = { name: title, description, media: cid?.cid.toString(), reference: "", } const payload = { Mint: { to: account?.decodedAddress, tokenMetadata, } }; handleMessage( payload, { onSuccess: () => { resetForm(); navigate('/') }, }, ); } ... } ``` The `CreateNft` page is ready. The whole code is: ```typescript import { useAccount } from '@gear-js/react-hooks'; import { Button, FileInput, Input } from '@gear-js/ui' import { useIPFS } from 'hooks'; import { useSendNFTMessage } from 'hooks/api'; import { useState } from 'react' import { useNavigate } from 'react-router-dom'; import styles from './CreateNft.module.scss' const NftInitialState = { title: "", description: "", } export function CreateNft() { const [nftForm, setNftForm] = useState(NftInitialState); const [image, setImage] = useState<File | null>(null) const { title, description } = nftForm; const handleInputChange = (e: {target: {name: any, value: any }}) => { const { name, value } = e.target; setNftForm({...nftForm, [name]: value}) } const ipfs = useIPFS(); const { account }= useAccount(); const navigate = useNavigate(); const handleMessage = useSendNFTMessage(); const resetForm = () => { setNftForm(NftInitialState); setImage(null) } const createNft = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); let cid; if (image) { try { cid = await ipfs.add(image as File) } catch (error) { alert(error) } } const tokenMetadata = { name: title, description, media: cid?.cid.toString(), reference: "", } const payload = { Mint: { to: account?.decodedAddress, tokenMetadata, } }; handleMessage( payload, { onSuccess: () => { resetForm(); navigate('/') }, }, ); } return ( <> <h2 className={styles.heading}> Create NFT</h2> <div className={styles.main}> <form className={styles.from} onSubmit={createNft}> <div className={styles.item}> <Input label="Name" className={styles.input} required name="title" value={title} onChange={handleInputChange}/> </div> <div className={styles.item}> <Input label="Description" className={styles.input} required name="description" value={description} onChange={handleInputChange}/> </div> <div className={styles.item}> <FileInput label="Image" className={styles.input} onChange={setImage}/> { image ? ( <div className="image-preview"> <img src={URL.createObjectURL(image)} alt="nft" style={{width: 100, height: 100}}/> </div> ): ( <p>No image set for this NFT</p> )} </div> <Button type="submit" text="Create" className={styles.button}/> </form> </div> </> ) } ``` In the next section we'll create the `Home` page where we'll read and display the minted NFTs. ### Home page In the `api.ts` file we'll add hooks for reading the contract state. First let's add `useNFTState<T>`, where `T` is a type that we're expecting to read (for example `Token`). It'll accept the function name and payload if it's required for the indicated function: ```typescript import stateMetaWasm from 'assets/wasm/nft_state.meta.wasm' import { useMetadata, useWasmMetadata } from './useMetadata' import metaTxt from 'assets/meta/meta.txt' import { useAccount, useReadWasmState, useSendMessage } from '@gear-js/react-hooks'; import { ADDRESS } from 'consts'; function useNFTMetadata() { return useMetadata(metaTxt); } function useNFTState<T>(functionName: string, payload?: any) { const { buffer } = useWasmMetadata(stateMetaWasm); return useReadWasmState<T>( ADDRESS.CONTRACT_ADDRESS, buffer, functionName, payload ) } ``` Let's read all the tokens our contract has. At first, we'll create the type for token in the separate folder `types`: ``` mkdir types touch types/index.ts ``` and add to the `intex.ts` file the `Token` description: ```typescript import { HexString } from "@polkadot/util/types"; type Token = { approvedAccountIds: HexString[]; description: string; id: string; media: string; name: string; ownerId: HexString; reference: string; }; export type { Token }; ``` Then we can write `useNFTs` hook: ```typescript ... import { Token } from 'types'; ... function useNFTs() { const { state } = useNFTState<Token[]>("all_tokens", null); return state; } ``` Now let's start writing the `Home` page. ```typescript import { Loader } from 'components'; import { useNFTs } from 'hooks/api'; import styles from './Home.module.scss' function Home() { const nfts = useNFTs(); const isAnyNft = !!nfts?.length; return ( <> <header className={styles.header}> <h2 className={styles.heading}>NFTs</h2> </header> {nfts ? ( <> {isAnyNft && <ul className={styles.list}>Display NFTs here</ul>} {!isAnyNft && <h2>There are no NFTs at the moment</h2>} </> ) : ( <Loader /> )} </> ) } export { Home }; ``` We read `nfts` using the previously written hook `useNFTs`. ```typescript const nfts = useNFTs(); ``` Then we check whether the contract has tokens: ```typescript const isAnyNft = !!nfts?.length; ``` Let's create a component that will display NFT: ``` mkdir pages/home/nft touch pages/home/nft/nft.tsx ``` and write the component: ```typescript import { Link } from "react-router-dom"; import { getIpfsAddress } from "utils"; import styles from './nft.module.scss' type Props = { id: string; name: string; media: string } function NFT( {id, name, media }: Props) { const to = `/nft/${id}`; const src = getIpfsAddress(media) const text = `#${id}` return ( <Link to={to} className={styles.nft}> <img src={src} alt={name}/> <h3 className={styles.heading}>{name}</h3> <p className={styles.text}>{text}</p> </Link> ) } export { NFT }; ``` Then let's write a function for getting all NFTs from the contract in the `Home.tsx`: ```typescript ... import { NFT } from './nft/nft'; function Home() { const nfts = useNFTs(); const isAnyNft = nfts?.length; const getNFTs = () => nfts?.map( ({name, id, media}) => ( <li key={id}> <NFT id = {id} name = {name} media = {media} /> </li> )) ... } export { Home }; ``` The whole code of the `Home` page: ```typescript import { Loader } from 'components'; import { useNFTs } from 'hooks/api'; import styles from './Home.module.scss' import { NFT } from './nft/nft'; function Home() { const nfts = useNFTs(); const isAnyNft = nfts?.length; const getNFTs = () => nfts?.map( ({name, id, media}) => ( <li key={id}> <NFT id = {id} name = {name} media = {media} /> </li> )) const NFTs = getNFTs(); return ( <> <header className={styles.header}> <h2 className={styles.heading}>NFTs</h2> </header> {nfts ? ( <> {isAnyNft && <ul className={styles.list}>{NFTs}</ul>} {!isAnyNft && <h2>There are no NFTs at the moment</h2>} </> ) : ( <Loader /> )} </> ) } export { Home }; ```