diff --git a/sidebar.json b/sidebar.json
index 586fb089..d93a4f34 100644
--- a/sidebar.json
+++ b/sidebar.json
@@ -502,9 +502,13 @@
"slug": "41-build-space-invaders-with-kaboom"
},
{
- "name": "Robot NFT on Ethereum blockchain",
+ "name": "Robot NFT Part 1: on Ethereum blockchain",
"slug": "42-build-a-robot-nft"
},
+ {
+ "name": "Robot NFT Part 2: Frontend",
+ "slug": "43-build-a-robot-nft-p2"
+ },
{
"name": "Pinboard project part 1: HTML & CSS",
"slug": "pinboard-project-part-1"
diff --git a/static/images/tutorials/43-robot-nft-p2/copy-addr.png b/static/images/tutorials/43-robot-nft-p2/copy-addr.png
new file mode 100644
index 00000000..add08f43
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/copy-addr.png differ
diff --git a/static/images/tutorials/43-robot-nft-p2/final-dapp.png b/static/images/tutorials/43-robot-nft-p2/final-dapp.png
new file mode 100644
index 00000000..f7373d13
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/final-dapp.png differ
diff --git a/static/images/tutorials/43-robot-nft-p2/frontend-svg-dir.mp4 b/static/images/tutorials/43-robot-nft-p2/frontend-svg-dir.mp4
new file mode 100644
index 00000000..0e54f4ab
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/frontend-svg-dir.mp4 differ
diff --git a/static/images/tutorials/43-robot-nft-p2/frontend-svg-dir.png b/static/images/tutorials/43-robot-nft-p2/frontend-svg-dir.png
new file mode 100644
index 00000000..2aeb9769
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/frontend-svg-dir.png differ
diff --git a/static/images/tutorials/43-robot-nft-p2/get-one.png b/static/images/tutorials/43-robot-nft-p2/get-one.png
new file mode 100644
index 00000000..20aada5c
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/get-one.png differ
diff --git a/static/images/tutorials/43-robot-nft-p2/repl-eth-web.png b/static/images/tutorials/43-robot-nft-p2/repl-eth-web.png
new file mode 100644
index 00000000..77ea83c8
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/repl-eth-web.png differ
diff --git a/static/images/tutorials/43-robot-nft-p2/show-config.mp4 b/static/images/tutorials/43-robot-nft-p2/show-config.mp4
new file mode 100644
index 00000000..22cf7cf6
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/show-config.mp4 differ
diff --git a/static/images/tutorials/43-robot-nft-p2/show-config.png b/static/images/tutorials/43-robot-nft-p2/show-config.png
new file mode 100644
index 00000000..f7b6b3ab
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/show-config.png differ
diff --git a/static/images/tutorials/43-robot-nft-p2/svg-inside.png b/static/images/tutorials/43-robot-nft-p2/svg-inside.png
new file mode 100644
index 00000000..9614f070
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/svg-inside.png differ
diff --git a/static/images/tutorials/43-robot-nft-p2/switch-to-test.png b/static/images/tutorials/43-robot-nft-p2/switch-to-test.png
new file mode 100644
index 00000000..6705dab0
Binary files /dev/null and b/static/images/tutorials/43-robot-nft-p2/switch-to-test.png differ
diff --git a/static/tutorial-files/robot-nft-p2/replbot-art.zip b/static/tutorial-files/robot-nft-p2/replbot-art.zip
new file mode 100644
index 00000000..6e943dcc
Binary files /dev/null and b/static/tutorial-files/robot-nft-p2/replbot-art.zip differ
diff --git a/tutorials/43-build-a-robot-nft-p2.md b/tutorials/43-build-a-robot-nft-p2.md
new file mode 100644
index 00000000..050ac7c8
--- /dev/null
+++ b/tutorials/43-build-a-robot-nft-p2.md
@@ -0,0 +1,607 @@
+# Build a robot NFT on the Ethereum blockchain with Solidity and Replit (part 2)
+
+In the [first part of this tutorial](https://docs.replit.com/tutorials/42-build-a-robot-nft), we wrote an Ethereum smart contract for an NFT project called ReplBots – robot profile pictures with randomly generated colors and accessories. This contract allowed users to create ReplBot NFTs on the blockchain and forms the logic portion of our decentralized NFT application. In this second part of the tutorial, we'll create a web frontend for our smart contract, giving users a way to view their NFTs.
+
+By the end of this tutorial, you will know the basics of interacting with wallets and Ethereum smart contracts from JavaScript and building web3 applications.
+
+## Getting started
+
+Open the repl you created in part one of this tutorial, or clone ours:
+
+
+
+If you haven't already, [install Metamask](https://metamask.io/download/) in your browser.
+
+Once your repl is loaded, create a new directory named `frontend`. This directory will contain the HTML, JavaScript, and image files used for our frontend. Inside this directory, create a second directory named `svg`. Download [this ZIP folder containing our NFT art assets](/tutorial-files/robot-nft-p2/replbot-art.zip), unzip it, and upload its contents to the `svg` directory you just created.
+
+
+
+
+Inside the main `frontend` directory, create the following files:
+
+* `index.html`: This will be our app's single page.
+* `style.css`: This will contain our app's CSS stylesheet.
+* `app.js`: This will contain our app's JavaScript logic, used for connecting to the blockchain and constructing ReplBot images.
+
+Before we start populating these files, we need to have a ReplBots contract instance for our frontend to connect to. Let's deploy a new copy of our contract on the Replit Testnet using our Solidity repl's web interface.
+
+First, run your repl. Once all of the dependencies are installed, you should see the Replit Ethereum web interface in your repl's browser. It looks like this:
+
+
+
+Connect your MetaMask wallet to the web interface and switch to the Replit Testnet. If your balance is 0 ETH, click the link marked "Get 1 ETH for testing". Wait until 1 ETH shows up in your wallet balance on the top right of the page.
+
+
+
+
+Now you can deploy your contracts. Select "ReplBots" from the drop-down box and click **Deploy**. Approve the MetaMask pop-up that appears.
+
+Once this contract has been deployed, it will show up as an expandable box. If you expand the deployed contract, at the bottom, you'll see a value starting with `0x`: the contract's address. Click on this address to copy it to your clipboard. Then paste it in a safe place, such as the bottom of your repl's `README.md`.
+
+
+
+Next to the contract's address, you should see a button labeled "Copy ABI". Click on this button to copy the contract's ABI ([Application Binary Interface](https://docs.soliditylang.org/en/v0.8.13/abi-spec.html)) to your clipboard, and paste it into the same place you put the contract address. You should see a large JSON object.
+
+We'll need both of these pieces of data to interact with our contract later on.
+
+
+
+## Setting up the front-end
+
+Open `frontend/index.html` and populate it with the following markup:
+
+```html
+
+
+
+ ReplBot NFTs
+
+
+
+
+
+
+
+
+
+
+
+```
+
+In this file, we've created the following:
+
+* A `button` element for minting a new ReplBot.
+* A `form` element for breeding a new ReplBot from two parents.
+* A `div` element for displaying the user's owned ReplBots.
+
+We've also linked our stylesheet at the top of the file and our JavaScript file at the bottom.
+
+Add the following CSS to `frontend/style.css`:
+
+```css
+body {
+ font-family: monospace;
+}
+
+.button {
+ font-size: 1.1em;
+ border-width: 2px;
+ background-color: white;
+ margin: 1em;
+}
+
+form {
+ border: 4px solid black;
+ padding: 1em;
+ display: inline-block;
+}
+
+#bots {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+#bots > svg {
+ width: 30em;
+}
+```
+
+Finally, add this code skeleton to `frontend/app.js`:
+
+```javascript
+App = {
+ replbotAddress: "INSERT-CONTRACT-ADDRESS-HERE",
+ replbotContract: null,
+
+ init: async function() {},
+}
+
+App.init();
+```
+
+This `App` object will contain all of the status and functionality we'll implement. Insert the contract address you copied earlier as the value of `replbotAddress`.
+
+## Connecting to the blockchain
+
+The majority of Ethereum dapps use one of two libraries for interacting with the blockchain: [`web3.js`](https://web3js.readthedocs.io/en/v1.7.1/) or [`ethers.js`](https://docs.ethers.io/v5/). The `ethers.js` library was used for Replit's Solidity Starter interface, but we'll use `web3.js` for this tutorial.
+
+We can import `web3.js` by adding the following line to `index.html`, just above the line where we import `app.js`:
+
+```html
+
+```
+
+Then, returning to `frontend/app.js`, we'll connect to the blockchain in our app's `init` function. Add the following code to the function's body:
+
+```javascript
+ init: async function() {
+
+ if (window.ethereum) {
+ await window.ethereum.request({ method: 'eth_requestAccounts' });
+ window.web3 = new Web3(window.ethereum);
+ }
+ },
+```
+
+This code will interface with MetaMask and show a pop-up window asking the user to connect their wallet to our dapp. If the user accepts, we'll instantiate a `Web3` object using their account details.
+
+The majority of `web3.js` functions are [asynchronous](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), so we have to use [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) to get their return values. As `await` can only be used in asynchronous functions or top-level JavaScript, the majority of our functions will also be asynchronous.
+
+Because we're building on the Replit Testnet, we'll need to add some code that prompts the user to switch to this network. Add the following function definition just below the definition of `init`:
+
+```javascript
+ switchToReplitTestnet: function() {
+ window.ethereum.request({
+ method: "wallet_addEthereumChain",
+ params: [
+ {
+ chainId: "0x7265706c",
+ chainName: "Replit Testnet",
+ rpcUrls: ["https://eth.replit.com"],
+ iconUrls: [
+ "https://upload.wikimedia.org/wikipedia/commons/b/b2/Repl.it_logo.svg",
+ ],
+ nativeCurrency: {
+ name: "Replit ETH",
+ symbol: "RΞ",
+ decimals: 18,
+ },
+ },
+ ],
+ });
+ },
+```
+
+This code provides the details MetaMask needs to prompt the user to switch networks. We'll invoke it from our `init` function, as below:
+
+```javascript
+ init: async function() {
+
+ if (window.ethereum) {
+ await window.ethereum.request({ method: 'eth_requestAccounts' });
+ window.web3 = new Web3(window.ethereum);
+
+ // NEW CODE BELOW
+ // Switch networks
+ App.switchToReplitTestnet();
+ }
+
+ },
+```
+
+## Interfacing with the smart contract
+
+Now that we can connect to the blockchain, we need to access our ReplBots contract. To connect to a contract and run its functions, we need two things: the contract's address, which shows us where to find it, and its ABI, which tells us what functions it implements and what their parameters are. We've already provided the address, so now we just need to specify the ABI.
+
+Create a new file in `frontend` called `replbotABI.js` and add the following code to it:
+
+```javascript
+const replbotABI =
+```
+
+Find the ABI data you copied after deploying the ReplBots contract and paste it in as the value of `replbotABI`. Once that's done, add the following line to `frontend/index.html`, just below the line where you import `web3`:
+
+```html
+
+```
+
+Now we can instantiate `App.replbotContract` as a [`web3.eth.Contract`](https://web3js.readthedocs.io/en/v1.7.1/web3-eth-contract.html). Amend your `init` function in `frontend/app.js` so that it matches the following:
+
+```javascript
+ init: async function() {
+
+ if (window.ethereum) {
+ await window.ethereum.request({ method: 'eth_requestAccounts' });
+ window.web3 = new Web3(window.ethereum);
+
+ // Switch networks
+ App.switchToReplitTestnet();
+
+ // NEW CODE BELOW
+ // Interface with contract
+ App.replbotContract = new web3.eth.Contract(replbotABI, App.replbotAddress);
+ }
+ },
+```
+
+That's all we need to connect to our contract. Now we can implement some of its functions on our frontend.
+
+## Running contract functions
+
+We'll start by implementing some functions in `App` to get information from the contract. This first one, `getMyReplBotIds`, will return a list of ReplBots in the current user's wallet. Add the following code within the definition of `App` at the bottom.
+
+```javascript
+ // view data in contract
+ getMyReplBotIds: async function() {
+ // get user's address
+ const accounts = await web3.eth.getAccounts();
+ const account = accounts[0];
+
+ // get number of ReplBots owned
+ let balance = await App.replbotContract.methods.balanceOf(account).call();
+
+ // get each one's ID
+ var botIds = [];
+ for (i = 0; i < balance; i++) {
+ botIds.push(await App.replbotContract.methods.tokenOfOwnerByIndex(account, i).call());
+ }
+
+ return botIds;
+ },
+```
+
+In this function, we use [`web3.eth.getAccounts()`](https://web3js.readthedocs.io/en/v1.7.1/web3-eth.html#getaccounts) to find the user's wallet address, which we use in subsequent calls to `balanceOf()` and `tokenOfOwnerByIndex()`. Both of these functions are part of the [ERC-721 NFT standard](https://eips.ethereum.org/EIPS/eip-721) (although the latter belongs to an optional extension, `ERC721Enumerable`) and are implemented by the OpenZeppelin contract our ReplBots contract inherits from. Note the syntax for calling a contract method using `call()`.
+
+Next, let's implement a function that returns details about a given ReplBot. Add the following code below the definition of `getMyReplBotIds()`:
+
+```javascript
+ getReplBotDetails: async function(tokenId) {
+ var bot = {};
+ bot.colors = await App.replbotContract.methods.botColors(tokenId).call();
+ bot.accessories = await App.replbotContract.methods.botAccessories(tokenId).call();
+ bot.parentage = await App.replbotContract.methods.botParentage(tokenId).call();
+ return bot;
+ },
+```
+
+This code is pretty simple. We've combined all three of our contract's bot information functions into a single function with a single return value.
+
+Those are the only view functions we'll need to worry about for now, so let's move on to functions that change state. First, we'll implement a function for `mint`. Add the following code to the bottom of your `App` definition:
+
+```javascript
+ // create new NFTs
+ mintReplBot: async function() {
+ const accounts = await web3.eth.getAccounts();
+ const account = accounts[0];
+
+ // Mint to own address
+ App.replbotContract.methods.mint(account).send({from: account});
+ },
+```
+
+When we call functions that return information without changing state, we can use `.call()` and don't have to specify a caller address. This is because external calls to view functions do not cost gas – they can be run on a single, local node instead of by the whole network. Functions that change state, however, must be run by the whole network, to reach consensus, and this costs gas. Therefore, we use `.send()` instead of `.call()`, and must specify a sender, who will need to pay for the gas.
+
+Our function for the `breed()` function is similar. Add the following code just below your `mintReplBot` definition:
+
+```javascript
+ breedReplBot: async function(parentOneId, parentTwoId) {
+ const accounts = await web3.eth.getAccounts();
+ const account = accounts[0];
+
+ await App.replbotContract.methods.breed(parentOneId, parentTwoId, account).send({from: account});
+ },
+```
+
+## Viewing and combining SVGs
+
+We've implemented the key contract functions above, including functions that will allow us to retrieve the details of individual ReplBot NFTs. For the moment, the details of these NFTs are just lists of strings. In this part of the tutorial, we'll turn those lists into unique images, using the powerful [SVG image format](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics).
+
+Most common image files, such as JPEGs and PNGs, are [raster graphics](https://en.wikipedia.org/wiki/Raster_graphics) – highly compressed representations of a grid of pixels. SVGs, on the other hand, are [vector graphics](https://en.wikipedia.org/wiki/Vector_graphics), made out of shapes. A good way to think about this is as follows: raster graphics files contain instructions like "color the pixel at (10,20) in red", whereas vector graphics files contain instructions like "draw a red line from point (10,12) to (20,14)".
+
+Both image formats have their advantages and disadvantages – a relevant advantage to SVGs is that they're formatted a lot like HTML files, so it's pretty simple to manipulate them programmatically in JavaScript. To see what an SVG looks like under the hood, rename one of the files in `frontend/svg` from `.svg` to `.html`.
+
+
+
+As you can see, an SVG is made up of a whole lot of XML tags. It even includes CSS for styling, inside the `` tag – we'll use this to change colors. When you're done looking at the file, change its file extension back to `.svg`.
+
+SVGs can be stored in external files or written inline in HTML. To keep our code clean, we'll keep them in external files. So the first thing we'll need is a function that fetches SVG data from external files. Open `frontend/app.js` and add the following helper function code above the definition of `App`:
+
+```javascript
+// helper function
+async function fetchSvg(filename) {
+ let svgFile = await fetch(`svg/${filename}`);
+ let svgText = await svgFile.text();
+
+ const parser = new DOMParser();
+ return parser.parseFromString(svgText, "text/html").getElementsByTagName("svg")[0];
+}
+```
+
+Here we use [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to retrieve a specified file from our `svg` directory. We then extract the file's text, which will be a string, and use a [`DOMParser`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser) to [convert this text to HTML](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString). This will allow us to manipulate it using standard JavaScript DOM functions.
+
+Now we have everything we need to write our SVG construction function. This function needs to do the following:
+
+1. Get the color and accessory details of a given tokenID.
+2. Fetch SVGs for the ReplBot's head and change its colors per the ReplBot's details.
+3. Fetch SVGs for the ReplBot's accessories and merge them with the ReplBot head.
+
+Add the code below within your `App` definition at the bottom:
+
+```javascript
+ // SVG handling
+ createReplBotSVG: async function(tokenId) {
+ // get bot details
+ let details = await App.getReplBotDetails(tokenId);
+
+ // get bothead
+ let botSvg = await fetchSvg("bothead.svg");
+
+ // change bot colors
+ botSvg.querySelectorAll('.frame').forEach(f => {
+ f.style.fill = `rgb${details.colors[0]}`;
+ });
+
+ botSvg.querySelectorAll('.visor').forEach(v => {
+ v.style.fill = `rgb${details.colors[1]}`;
+ });
+
+ botSvg.querySelectorAll('.background').forEach(b => {
+ b.style.fill = `rgb${details.colors[2]}`;
+ });
+ },
+```
+
+First, we get the ReplBot details and the base ReplBot SVG. We then apply the bot's colors by setting `style.fill` for every instance of each of the relevant classes, using [the CSS `rgb()` function](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb) with the color information from `details`.
+
+Next, we need to get the bot's accessories. Add the following code above this function's closing curly brace (`}`):
+
+```javascript
+ // get bot accessories
+ let accessorySvgs = [];
+ for (let i = 0; i < 3; i++) {
+ let filename = details.accessories[i].toLowerCase().replaceAll(" ","-") + ".svg";
+
+ let svg = await fetchSvg(filename);
+ accessorySvgs.push(svg);
+ }
+```
+
+Here we fetch each of the bot's three accessories and add them to a list. We get the filename for the accessory by transforming the string we received from `getReplBotDetails()`. So, for example, "Bunny Ears" becomes "bunny-ears.svg".
+
+Once we've got our accessory SVGs, we can merge them into the base ReplBot SVG. Enter the following code just below the `for` loop above:
+
+```javascript
+ // merge SVGs
+ accessorySvgs.forEach(a => {
+ Array.from(a.getElementsByTagName("style")).forEach(e => {
+ botSvg.getElementsByTagName("defs")[0].appendChild(e);
+ });
+
+ Array.from(a.getElementsByTagName("path")).forEach(e => {
+ botSvg.appendChild(e);
+ });
+
+ Array.from(a.getElementsByTagName("polyline")).forEach(e => {
+ botSvg.appendChild(e);
+ });
+
+ });
+```
+
+We merge SVGs by appending all of `