How to create an Instagram clone on the Polygon Network using Next.js and Truffle
Tags: web3, nextjs, blockchain, javascript
Published: 2022-10-12 17:56:12

Hey everyone, I will be walking you through the development of an Instagram clone using Next.js, TailwindCSS, Truffle, Web3.js, and IPFS-HTTP-CLIENT. Disclaimer: I am not focusing on the Front End styling. I will leave that up to you to style how you like :) I will primarily be focusing on the backend / Solidity.

**Setting up Next.js Project with TailwindCSS Template**

To begin, create a new Next.js Project with TailwindCSS. To easily set this up, open your terminal and enter the command 
`npx create-next-app@latest -e with-tailwindcss instagram-polygon`

**Installing the necessary packages**

Once the project has been initialized, follow these commands to get all the packages set up.
1. change into the project directory
`cd instagram-polygon`

2. install the required packages
`yarn add web3 truffle ipfs-http-client@33.0.1`

3. create a folder named **backend**

4. in the **backend** folder, run the command 
`npx truffle init`
to set up the Ethereum development environment and the necessary folders for contracts, migrations, and build.

Congrats! You have now set up the base of your Ethereum development folder.

**Truffle Setup**

To use Ganache as your own personal blockchain, please visit
**https://trufflesuite.com/ganache/**
and download and run Ganache.

Since we are using Truffle for development, I will only include the development network as we will add the Polygon configurations before deployment :)

In the **backend** folder, you should now see a file named **truffle-config.js**. Replace the contents of the file with the following:
```
module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*",
    },
  },

  mocha: {
    // timeout: 100000
  },

  // Configure your compilers
  compilers: {
    solc: {
      version: "0.8.15",
    },
  },
};
```
This allows us to use Ganache as our testing development. The compiler section of this file also indicates that we will be using Solidity version 0.8.15 for this project.

Now that you have your configuration file set up, we can begin working on the Smart Contract for this dApp.

**Setting up the smart contracts**

In the **backend/contracts** folder, create a new file named **Migrations.sol**. This file is used for Truffle to read the migration whenever we want to update or change any information of the smart contract.
In this file, add the following:
```
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Migrations {
    address public owner = msg.sender;
    uint256 public last_completed_migration;

    modifier restricted() {
        require(
            msg.sender == owner,
            "This function is restricted to the contract's owner"
        );
        _;
    }

    function setCompleted(uint256 completed) public restricted {
        last_completed_migration = completed;
    }
}
```
Now that we have our migrations contract set up, create a new file in the same **backend/contracts** folder named **Instagram.sol**. This smart contract is where we define what we want the application to do. We will be using the following structure:
1. A public integer postCount that tracks the total amount of posts
2. A mapping of the posts, which allows us to sort by id
3. Structs of the Post which define the necessary inputs/parameters
You can see this through the finished smart contract, which looks like:
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

contract Instagram{
    string public name = "Instagram";
    uint256 public postCount= 0;
    mapping(uint256 => Post) public posts;

    struct Post{
        uint256 id;
        string hash;
        string description;
        address payable author;
    }

    event PostCreated(
        uint256 id,
        string hash,
        string description,
        address payable author
    );

    constructor() {
        name = "Instagram";
    }

    function uploadPost(string memory _postHash, string memory _description)
        public
    {
        // Make sure the post hash exists
        require(bytes(_postHash).length > 0);
        // Make sure post description exists
        require(bytes(_description).length > 0);
        // Make sure uploader address exists
        require(msg.sender != address(0x0));
        // Increment post id
        postCount++;
        // Add post to contract
        posts[postCount] = Post(
            postCount,
            _postHash,
            _description,
            payable(msg.sender)
        );
        // Trigger an event
        emit PostCreated(
            postCount,
            _postHash,
            _description,
            payable(msg.sender)
        );
    }
}
```

Congrats! You now have a Smart Contract set up for this dApp.

Continuing, head over to the **backend/migrations** folder and create a file named **1_initial_migrations.js**. This file is used for the first migration to Truffle. Your file should look like this:
```
const Migrations = artifacts.require("Migrations");

module.exports = function (deployer) {
  deployer.deploy(Migrations);
};
```

Create another file in **backend/migrations** named **2_deploy.js**. This file contains the artifacts for the smart contract we just created. Your file should look like this:
```
const Instagram = artifacts.require("Instagram");

module.exports = function (deployer) {
  deployer.deploy(Instagram);
};
```
Once you have the files set up, open a terminal and **cd into the backend directory using cd/backend**

Run the command `npx truffle migrate --reset` to deploy the contract onto the development network.

**You have now officially set up the backend for the Instagram dApp!!!**


**Setting up the front end connection to the smart contract**

In the **pages** folder of the project, you should see **_app.tsx and index.tsx**. I personally do not enjoy using TypeScript so I change the files to .js and get rid of the :NextPage and :AppProps. This should have no negative effect to your project.

In **index.js**, import the following packages at the top of your file.
```
import Head from "next/head";
import { useEffect, useState } from "react";
import Web3 from "web3";
import Instagram from "../backend/build/contracts/Instagram.json";
```
We will not be using much, only a couple necessary packages to get up and running.

After importing the packages, we can start setting up the states for each function/component. Using useState, your file should contain the following just under the const Home statement.
```
const [account, setAccount] = useState("");
const [web3, setWeb3] = useState(null);
const [instagram, setInstagram] = useState(null);
const [postCount, setPostCount] = useState(0);
const [posts, setPosts] = useState([]);
const [buffer, setBuffer] = useState(null);
const [loading, setLoading] = useState(false);
```
This sets up the states for everything we need in our project.

Now we can create the functions to enable us to connect to the blockchain. Still in the **index.js** file, create a function called Web3Handler. This enables us to connect to MetaMask. The function looks like this:
```
const Web3Handler = async () => {
    try {
      const account = await window.ethereum.request({
        method: "eth_requestAccounts",
      });

      const web3 = new Web3(window.ethereum);
      setAccount(account[0]);
      setWeb3(web3);
    } catch (error) {
      console.log(error);
    }
  };
```
Once you have this, we will now set up the loadBlockchainData that allows us to use the contracts information. Here we set up a statement that alerts the user if they are not connected to the proper network. This function should look like this: 
```
const loadBlockchainData = async () => {
    const web3 = new Web3(window.ethereum);
    const accounts = await web3.eth.getAccounts();
    setAccount(accounts[0]);

    const networkId = await web3.eth.net.getId();
    const networkData = Instagram.networks[networkId];
    if (networkData) {
      const instagram= new web3.eth.Contract(Instagram.abi, networkData.address);
      setInstagram(instagram);
      const postCount= await instagram.methods.postCount().call();
      setPostCount(postCount);

      for (let i = postCount; i >= 1; i--) {
        const post = await instagram.methods.posts(i).call();
        setPosts((prevPosts) => [...prevPosts, post]);
      }
    } else {
      window.alert("Twitter contract not deployed to detected network.");
    }
  };
```

You should now have the 2 functions for the blockchain connection. 

Add the following useEffect to ensure the account only gets rendered on component load.
```
useEffect(() => {
    loadBlockchainData();
  }, [account]);
```

We will now create the ipfs-http-client handler.
Head over to https://www.infura.io and create an account / log in. Create a new project under IPFS, and name it whatever you would like. On this screen, you will find the projectId and projectSecret that is necessary for uploading files to IPFS.
Your index.js should contain the following:
```
const ipfsClient = require("ipfs-http-client");
  const projectId = "21sdaf....";
  const projectSecret = "22fsa...";
  const auth =
    "Basic " + Buffer.from(projectId + ":" + projectSecret).toString("base64");
  const client = ipfsClient({
    host: "ipfs.infura.io",
    port: 5001,
    protocol: "https",
    headers: {
      authorization: auth,
    },
  });

```
This properly sets up the connection between IPFS, Infura, and your project.

We can now create the 2 functions that deal with uploading your post to the blockchain. 

First we create the function captureFile. This function handles the attachment of the file. This should look like:
```
const captureFile = (event) => {
    event.preventDefault();
    const file = event.target.files[0];
    const reader = new window.FileReader();
    reader.readAsArrayBuffer(file);
    reader.onloadend = () => {
      setBuffer(Buffer(reader.result));
      console.log("buffer", buffer);
    };
  };
```


We also have to create a function that uploads the post, uploadPost. This should look like:
```
 const uploadPost = async (description) => {
    setLoading(true);
    try {
      client.add(buffer, (error, result) => {
        console.log("IPFS result", result);

        if (error) {
          console.error(error);
          return;
        }

        setLoading(true);
        instagram.methods
          .uploadPost(result[0].hash, description)
          .send({ from: account })
          .on("transactionHash", (hash) => {
            setLoading(false);
          });
      });
    } catch (error) {
      console.log(error);
      setLoading(false);
    }
  };
```
You now have all the functions to get this dApp working!
Your final index.js file should look like this:
```
import Head from "next/head";
import { useEffect, useState } from "react";
import Web3 from "web3";
import Instagram from "../backend/build/contracts/Instagram.json";

const Home = () => {
const [account, setAccount] = useState("");
const [web3, setWeb3] = useState(null);
const [instagram, setInstagram] = useState(null);
const [postCount, setPostCount] = useState(0);
const [posts, setPosts] = useState([]);
const [buffer, setBuffer] = useState(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
    loadBlockchainData();
  }, [account]);

const ipfsClient = require("ipfs-http-client");
  const projectId = "21sdaf....";
  const projectSecret = "22fsa...";
  const auth =
    "Basic " + Buffer.from(projectId + ":" + projectSecret).toString("base64");
  const client = ipfsClient({
    host: "ipfs.infura.io",
    port: 5001,
    protocol: "https",
    headers: {
      authorization: auth,
    },
  });

const Web3Handler = async () => {
    try {
      const account = await window.ethereum.request({
        method: "eth_requestAccounts",
      });

      const web3 = new Web3(window.ethereum);
      setAccount(account[0]);
      setWeb3(web3);
    } catch (error) {
      console.log(error);
    }
  };

const loadBlockchainData = async () => {
    const web3 = new Web3(window.ethereum);
    const accounts = await web3.eth.getAccounts();
    setAccount(accounts[0]);

    const networkId = await web3.eth.net.getId();
    const networkData = Instagram.networks[networkId];
    if (networkData) {
      const instagram= new web3.eth.Contract(Instagram.abi, networkData.address);
      setInstagram(instagram);
      const postCount= await instagram.methods.postCount().call();
      setPostCount(postCount);

      for (let i = postCount; i >= 1; i--) {
        const post = await instagram.methods.posts(i).call();
        setPosts((prevPosts) => [...prevPosts, post]);
      }
    } else {
      window.alert("Twitter contract not deployed to detected network.");
    }
}

const captureFile = (event) => {
    event.preventDefault();
    const file = event.target.files[0];
    const reader = new window.FileReader();
    reader.readAsArrayBuffer(file);
    reader.onloadend = () => {
      setBuffer(Buffer(reader.result));
      console.log("buffer", buffer);
    };
  };

const uploadPost = async (description) => {
    setLoading(true);
    try {
      client.add(buffer, (error, result) => {
        console.log("IPFS result", result);

        if (error) {
          console.error(error);
          return;
        }

        setLoading(true);
        instagram.methods
          .uploadPost(result[0].hash, description)
          .send({ from: account })
          .on("transactionHash", (hash) => {
            setLoading(false);
          });
      });
    } catch (error) {
      console.log(error);
      setLoading(false);
    }
  };

return (
    <div>
      <h1>yo</h1>
    </div>
  );
};

export default Home;
```

Congrats! Your functions are all complete and now we are ready to create the front end.

In the empty div in the return above, we can add the following code:
```
<button onClick={Web3Handler}>Connect Wallet</button>
      {account && <h1>{account}</h1>}
```
This will connect the user to MetaMask and also display the connected accounts address.

Now to create the form that handles uploads:
```
<form
        onSubmit={(event) => {
          event.preventDefault();
          const description = event.target.imageDescription.value;
          uploadPost(description);
        }}
      >
    
        <input
 
          id="file_input"
          type="file"
          onChange={captureFile}
        />
        <input
          id="imageDescription"
          type="text"
         
          placeholder="Image description..."
          required
        />
        <div>
          <button type="submit">
            upload
          </button>
        </div>
        {loading && <h1>Loading...</h1>}
      </form>
```
This should properly allow you to attach a file, enter a description and upload the post to the blockchain.

In order to render the posts on the page, include the following code:
```
{posts &&
        posts.map((post, key) => {
          return (
            <div key={key}>
              <div>
                <h2>{post.description}</h2>
                <img src={`https://ipfs.io/ipfs/${post.hash}`} />
              </div>
            </div>
          );
        })}
```


**YOUR FINAL FINAL index.js FILE SHOULD LOOK LIKE THIS:**
```
import Head from "next/head";
import { useEffect, useState } from "react";
import Web3 from "web3";
import Instagram from "../backend/build/contracts/Instagram.json";

const Home = () => {
  const [account, setAccount] = useState("");
  const [web3, setWeb3] = useState(null);
  const [instagram, setInstagram] = useState(null);
  const [postCount, setPostCount] = useState(0);
  const [posts, setPosts] = useState([]);
  const [buffer, setBuffer] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    loadBlockchainData();
  }, [account]);

  const ipfsClient = require("ipfs-http-client");
  const projectId = "2FdliMGfWHQCzVYTtFlGQsknZvb";
  const projectSecret = "2274a79139ff6fdb2f016d12f713dca1";
  const auth =
    "Basic " + Buffer.from(projectId + ":" + projectSecret).toString("base64");

  const client = ipfsClient({
    host: "ipfs.infura.io",
    port: 5001,
    protocol: "https",
    headers: {
      authorization: auth,
    },
  });

  const Web3Handler = async () => {
    try {
      const account = await window.ethereum.request({
        method: "eth_requestAccounts",
      });

      const web3 = new Web3(window.ethereum);
      setAccount(account[0]);
      setWeb3(web3);
    } catch (error) {
      console.log(error);
    }
  };

  const loadBlockchainData = async () => {
    const web3 = new Web3(window.ethereum);
    const accounts = await web3.eth.getAccounts();
    setAccount(accounts[0]);

    const networkId = await web3.eth.net.getId();
    const networkData = Instagram.networks[networkId];
    if (networkData) {
      const instagram = new web3.eth.Contract(
        Instagram.abi,
        networkData.address
      );
      setInstagram(instagram);
      const postCount = await instagram.methods.postCount().call();
      setPostCount(postCount);

      for (let i = postCount; i >= 1; i--) {
        const post = await instagram.methods.posts(i).call();
        setPosts((prevPosts) => [...prevPosts, post]);
      }
    } else {
      window.alert("Twitter contract not deployed to detected network.");
    }
  };

  const captureFile = (event) => {
    event.preventDefault();
    const file = event.target.files[0];
    const reader = new window.FileReader();
    reader.readAsArrayBuffer(file);
    reader.onloadend = () => {
      setBuffer(Buffer(reader.result));
      console.log("buffer", buffer);
    };
  };

  const uploadPost = async (description) => {
    setLoading(true);
    try {
      client.add(buffer, (error, result) => {
        console.log("IPFS result", result);

        if (error) {
          console.error(error);
          return;
        }

        setLoading(true);
        instagram.methods
          .uploadPost(result[0].hash, description)
          .send({ from: account })
          .on("transactionHash", (hash) => {
            setLoading(false);
            setTimeout(() => {
              window.location.reload();
            }, 2000);
          });
      });

      // wait 2 seconds than reload page
    } catch (error) {
      console.log(error);
      setLoading(false);
    }
  };

  return (
    <div>
      <button onClick={Web3Handler}>Connect Wallet</button>
      {account && <h1>{account}</h1>}

      <form
        onSubmit={(event) => {
          event.preventDefault();
          const description = event.target.imageDescription.value;
          uploadPost(description);
        }}
      >
        <input id="file_input" type="file" onChange={captureFile} />
        <input
          id="imageDescription"
          type="text"
          placeholder="Image description..."
          required
        />
        <div>
          <button className="border border-black" type="submit">
            upload
          </button>
        </div>
        {loading && <h1>Loading...</h1>}
      </form>

      {posts &&
        posts.map((post, key) => {
          return (
            <div key={key}>
              <h2>{post.description}</h2>
              <img src={`https://ipfs.io/ipfs/${post.hash}`} />
            </div>
          );
        })}
    </div>
  );
};

export default Home;
```

If you made it this far, thank you so much for reading along and following. I hope you learned something new! :)