DEV Community

Cover image for Build a CRUD DApp using Solidity | Nextjs | Thirdweb | Tailwind CSS
Mohamed Elgazzar
Mohamed Elgazzar

Posted on • Updated on

Build a CRUD DApp using Solidity | Nextjs | Thirdweb | Tailwind CSS

Hey there, today we are going to be learning how to create the CRUD operations in Web3 by building a simple Blog DApp. In this tutorial we will:

  1. Build a Solidity smart contract that will hold the blog's content and allow users to interact with it.
  2. Deploy the smart contract to the Mumbai TestNet, and perform the Web3 interactions on the Frontend using Thirdweb.
  3. Build the frontend using Nextjs.
  4. Style our blog using Tailwind CSS.

By the end of that tutorial, you'll have a full-stack CRUD DApp, that you can build your Web3 knowledge upon, whether you are new to the technology, or just need to refresh your basics.

Without further ado, let's dive right in and get started!

Let's start by opening up the terminal and creating a new directory for our project

mkdir Next3Blog && cd Next3Blog

Here we are calling it Next3Blog but you can call it whatever you want.

After that, we are going to create a new Thirdweb project, that will contain our smart contract logic

Run the following command

npx thirdweb@latest create --contract

It should prompt a few questions that will help setup the environment based on our needs

Image description

I set the project name to web3, picked Hardhat as our development environment framework, set BlogContract as the name of our smart contract, and finally, chose to initiate the project with an empty contract.

Navigate to the newly generated folder and take a look at the directory structure

cd web3

After navigating to the project folder, you will find the contracts directory which contains a single smart contract.

Building a Smart Contract using Solidity:

Solidity is a powerful high-level programming language. It's very similar to JavaScript, but it's essentially designed and used to write smart contracts on the Ethereum blockchain.

Open the contract file and copy the following code into it

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;


contract BlogContract {
   struct Post {
        uint postId;
        address owner;
        string postContent;
        bool isDeleted;
    }

    mapping(uint => address) blogOwners;
    Post[] private posts;
}

Enter fullscreen mode Exit fullscreen mode

We start by specifying the compiler version for the smart contract.

Then, we declare Post struct as a custom data type for our blog posts. It contains four variables, postId as uint which represents an unsigned (positive) integer, postContent as a string, isDeleted as a boolean.

mapping is a hash table that allows us to associate a key with a value, so in the above code we define a blogOwners mapping of a unit key, and address value.

After that, we declare a private posts array of type Post. The private keyword makes the array inaccessible from outside the contract.

Adding posts:

  function addPost(string memory postContent) external {
       uint postId = posts.length;
       address owner = msg.sender;
       posts.push(Post(postId, owner, postContent, false));
       blogOwners[postId] = owner;
   }

Enter fullscreen mode Exit fullscreen mode

In Solidity, a function parameter must be declared with a specific data type since it's a typed language.

In the above code, we are defining the addPost function, with a string parameter. memory is used to store data temporarily during the execution of a function, and will be deleted from memory once the function returns.

external makes the function visible from outside the contract only, and cannot be called from within the contract itself.

We then assign a couple of variables, postId of type unit, and owner of type address.

msg.sender refers to the address that called the function.

After that, we use the Post struct to define the post variables and push it to the posts array. And finally, we map the postId to the blog owner address.

In Solidity, events are used as a way to notify external systems or contracts about specific actions that occur within a contract.

Let's assign the AddPost event that will serve as a notifier when a new post is registered.

contract BlogContract {
   event AddPost(address owner, uint postId);
   ...
}

Enter fullscreen mode Exit fullscreen mode

Now we add emit to the addPost function to trigger the event.

function addPost(string memory postContent) external {
       ...
       emit AddPost(owner, postId);
}
Enter fullscreen mode Exit fullscreen mode

Deleting posts:

function deletePost(uint postId) external {
       require(blogOwners[postId] == msg.sender, "You are not the owner of this post");
       posts[postId].isDeleted = true;
   }

Enter fullscreen mode Exit fullscreen mode

Here we are defining a function that takes a single parameter postId of type uint, and then checking if the caller address is the owner of the post. require checks for the validity of the condition and it will revert the transaction and display the error message that we set.

Once data is written to the blockchain, it can’t be deleted as it becomes a permanent part of the blockchain’s history. However, we can "soft-delete" it. Remember the variable isDeleted that we added above? We are initially assigning it to false, and we will simply modify it to be true, which will create an illusion of deleting it.

Later on, when implementing the getPosts function we will filter the data, and only return data with the isDeleted set to false.

Let's now declare the DeletePost event


contract BlogContract {
   ...
   event DeletePost(uint postId, bool isDeleted);
   ...

   function deletePost(uint postId) external {
      ...
      emit DeletePost(postId, true);
   }

}
Enter fullscreen mode Exit fullscreen mode

Getting posts:

function getPosts() external view returns (Post[] memory){
        Post[] memory temporary = new Post[](posts.length);
        uint counter = 0;
        for (uint i = 0; i < posts.length; i++) {
            if (posts[i].isDeleted == false) {
                temporary[counter] = posts[i];
                counter++;
            }
        }
        Post[] memory result = new Post[](counter);
        for (uint i = 0; i < counter; i++) {
            result[i] = temporary[i];
        }
        return result;
    }
Enter fullscreen mode Exit fullscreen mode

A view function allows read-only access to the data stored in the contract and since it doesn't modify the state of the contract or blockchain, it doesn't require any transaction to be sent to the blockchain making it a lightweight operation.

As I mentioned earlier, we are gonna loop through the posts array, and only return posts with the isDeleted set to false.

First, we declare a temporary array of type Post, with a size equal to the length of the posts array.

Note that when declaring an array with the memory keyword, you need to specify the size of the array because Solidity doesn't support dynamic array resizing in memory.

Then we assign a counter variable that will track the number of the non "deleted" posts.

Then we loop through the posts array and store the non deleted posts inside the temporary array.

Since the temporary array may still have extra slots for the deleted posts, we need to declare another array of length counter to ensure that the final result array is of the exact size of the non-deleted posts.

Now that we finished implementing the contract, we need to deploy it to the blockchain. In the project directory you will find hardhat.config.js, this file contains all of our configuration settings of our development environment.

Navigate to the file and copy the code below into it:

module.exports = {
  solidity: {
    version: '0.8.9',
    defaultNetwork: 'mumbai',
    networks: {
      hardhat: {},
      mumbai: {
        url: 'https://rpc.ankr.com/polygon_mumbai',
        accounts: [`0x${process.env.PRIVATE_KEY}`]
      }
    },
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Here we are specifying the settings for the Hardhat project.

We declare the version of the compiler, the defaultNetwork as the Mumbai Testnet and defining the network configurations, we specify the URL of the RPC endpoint "https://rpc.ankr.com/polygon_mumbai" and the account to use for deployment.

optimizer is a tool that tries to reduce the gas cost of a smart contract by optimizing its bytecode, and setting the optimizer to 200 means that it will run the optimizer for a maximum of 200 times. The more times the optimizer runs, the more aggressive it is in optimizing the bytecode, but it also takes longer to compile the contract.

Now head to your browser and download the MetaMask wallet extension.

Follow the instructions to create a new wallet and don't forget to save the secret recovery phrase.

After you successfully set up your wallet, head to the extension and add the Mumbai TestNet to your networks.

Image description

  1. Network name: Mumbai TestNet
  2. New RPC URL: https://rpc.ankr.com/polygon_mumbai
  3. Chain ID: 80001
  4. Currency symbol: MATIC

Now copy the private key as shown here. That will give access to the wallet account and enable us to deploy the contract using it

Image description

For security purposes we need to add the private key into .env file

We need to install dotenv to be able to load and manage the environment variables from the .env file into the process environment.

npm install dotenv --save

When deploying the contract, it requires a certain amount of MATIC to cover the transaction fees.

Let's now get some MATIC faucet (https://faucet.polygon.technology/)

Head to https://faucet.polygon.technology and copy your wallet address into the input, click submit, and you should get a small amount of MATIC in a few moments.

Image description

Image description

This should be sufficient to deploy our contract.

Now open your terminal and run the deployment command

npm run deploy

This is a thirdweb script that will detect any contract inside the directory and compile it, upload the ABI to IPFS and generate a deployment link.

Image description

Open the link in your browser and click the deploy button to deploy the contract to the blockchain

Image description

Image description

Thirdweb will generate a dashboard to manage and interact with the deployed contract conveniently. Copy the contract address and put it to the side now, we are gonna use it later when we move on to the Frontend part.

Image description

Navigate to Explorer and play around with the contract functions

Image description

Nextjs

Next.js is an open-source React framework that provides server-side rendering, and makes it easier to build and optimize complex web applications.

We'll start by setting up our development environment and create a Nextjs app

Navigate to the root directory and copy the command below into your terminal

npx create-next-app@latest client

You will be prompted with a bunch of questions in the terminal

Image description

After it finishes installing the project dependencies, navigate to the project directory.

Tailwind CSS

Tailwind CSS is a utility-first CSS framework and a design system that provides you with pre-built classes to style your elements and create consistent and responsive web pages.

So, let's say you have a button element and you want to change its background color to gray, you can simply add bg-gray-500 - Assigning the value as 500 means that the color is gonna have a mid-level shade.

Setting up Tailwind CSS:

Let's first install the Tailwind CSS dependencies

npm install -D tailwindcss postcss autoprefixer

PostCSS and autoprefixer apply additional optimizations and ensure that the generated classes work on all browsers without worrying about the compatibility issues.

Initialize Tailwind CSS in our project

npx tailwindcss init -p

That will generate two files npm tailwind.config.js and postcss.config.js.

Inside tailwind.config.js copy the code below

module.exports = {
  content: [
    "./src/**/*.{js,jsx}",
  ],
  theme: {
    extend: {
      fontFamily: {
        openSans: ['Open Sans', 'sans-serif']
      }
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we are adding the paths to all of the expected template files to contain Tailwind styles, and the font we're gonna use in the project.

Then you navigate to ./src/styles/globals.css and add the Tailwind directives and the link for the font.

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Now we are ready to start building our UI

Here is a closer look to the final view of what we are building

Image description

In the pages directory, you will find index.js. This will be our main page.

In the src directory create components directory, and inside of it create four new files Navbar.js, Form.js, Post.js, and index.js.

Add a utils folder and inside of it create index.js

By now the src directory tree should look like this

Image description

Copy the code below into ./pages/index.js

export default function App() {

  return (
    <div className="bg-[#eff2f5] flex flex-col min-h-screen">



    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the tailwind classes that we are adding here are self-explanatory, we are simply setting the background color to #eff2f5, the display to flex, the flex direction to flex column, and the min height to 100vh.

Let's install an additional package before we implement the Navbar

npm i react-identicons --save

Identicons provide a visual representation to wallet addresses. They are typically generated using an algorithm that takes the wallet address as input and produces a unique image based on that input.

Navigate to Navbar.js and copy the following code into it

import Identicon from 'react-identicons';
export default function Navbar() {
 return (
   <div className="bg-white">
       <div className="px-5 py-2 flex items-center justify-between shadow-md">
           <h2 className="font-openSans font-bold text-gray-600 font-medium">
               Next3Blog
           </h2>
           <div className="border border-[#c7cacd] text-gray-600 px-2 py-1 rounded-[12px] inline-flex items-center">
               <div className="w-8 h-8 mr-2 rounded-full overflow-hidden border border-[#c7cacd] flex justify-center ">
                   <Identicon string={"0x34710CdeC3e80A174cb384870B3Ff2854d5556ce"} size={30}/>
               </div>
               <h2 className="font-openSans text-black text-[12px]">0x34710CdeC3e80A174cb384870B3Ff2854d5556ce</h2>
           </div>   
       </div>
   </div>
 )
}

Enter fullscreen mode Exit fullscreen mode

Notice how we are using the font that we previously declared in the tailwind.config.js.

To save space, and make the UI more compact, let's shorten the wallet address that we are displaying on the Navbar. This is a convention in DApps to represent the address in a user-friendly way.

Navigate to ./utils/index.js and add the following function

export const truncateAddress = (address) => {
   return address.substring(0, 6) + '...' + address.substring(address.length - 4, address.length)
};
Enter fullscreen mode Exit fullscreen mode

This truncates the address to show the first 6 characters and the last 4 characters, with an ellipsis (...) in between.

Let's import the function in Navbar.js and call the function with the temporary address.

...               
<h2 className="font-openSans text-black text-[12px]">{truncateAddress(address)}</h2>
...

Enter fullscreen mode Exit fullscreen mode

Next, navigate to Form.js

import Identicon from 'react-identicons';
export default function Form() {
 return (
   <form className="lg:w-1/2 flex flex-col w-full h-full bg-white overflow-hidden mb-[50px] shadow-md rounded-[8px] py-5 px-10 box-border">
       <div className="w-full inline-flex justify-between items-center mb-4">
       <div className="w-12 h-12 rounded-full overflow-hidden border border-[#c7cacd] flex justify-center ">
           <Identicon string={"0x34710CdeC3e80A174cb384870B3Ff2854d5556ce"} size={45}/>
       </div>
       <div className="w-4/5 lg:w-[90%]">
           <input type="text" id="post" className="font-openSans w-full p-3 text-md border rounded-[22px] focus:outline-none focus:shadow-outline" placeholder="Share Your Thoughts and Ideas!" />
       </div>
       </div>
       <div className="w-full justify-center items-center">
       <button className="w-full bg-[#1974E7] px-2 py-2 rounded-[6px] flex justify-center cursor-pointer disabled:opacity-50 disabled:cursor-default" type='submit'>
           <h2 className='font-openSans text-white transition'>Publish</h2>
       </button>
       </div>
   </form>
 )
}

Enter fullscreen mode Exit fullscreen mode

w-4/5: Sets the width to 75%
lg:w-[90%]: Sets the width to 90% on large screens

Navigate to Post.js

import Identicon from 'react-identicons';
import { truncateAddress } from '@/utils';


export default function Post() {
 return (
   <div className="lg:w-1/2 flex flex-col w-full h-full bg-white overflow-hidden mb-6 shadow-md rounded-[8px] pt-2 pb-5 px-6 box-border">
       <div className="w-full inline-flex justify-between items-center mb-4">
         <div className="inline-flex items-center">
           <div className="w-[40px] h-[40px] flex justify-center rounded-full overflow-hidden flex-shrink-0 border border-[#c7cacd]">
             <Identicon string={"0x34710CdeC3e80A174cb384870B3Ff2854d5556ce"} size={35} />
           </div>
           <div className="flex-1 pl-2">
             <h2 className="font-openSans text-black mb-1">{truncateAddress("0x34710CdeC3e80A174cb384870B3Ff2854d5556ce")}</h2>
           </div>
         </div>
           <button className="w-[10px] cursor-pointer">
             <svg className="fill-current w-3 h-3" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="122.878px" height="122.88px" viewBox="0 0 122.878 122.88" enableBackground="new 0 0 122.878 122.88" xml-space="preserve">
               <g>
                 <path d="M1.426,8.313c-1.901-1.901-1.901-4.984,0-6.886c1.901-1.902,4.984-1.902,6.886,0l53.127,53.127l53.127-53.127 c1.901-1.902,4.984-1.902,6.887,0c1.901,1.901,1.901,4.985,0,6.886L68.324,61.439l53.128,53.128c1.901,1.901,1.901,4.984,0,6.886 c-1.902,1.902-4.985,1.902-6.887,0L61.438,68.326L8.312,121.453c-1.901,1.902-4.984,1.902-6.886,0 c-1.901-1.901-1.901-4.984,0-6.886l53.127-53.128L1.426,8.313L1.426,8.313z"></path>
               </g>
             </svg>
           </button>
       </div>
       <div className="w-full justify-center items-center">
         <p className='font-openSans'>Sample Blog post</p>
       </div>
     </div>
 )
}

Enter fullscreen mode Exit fullscreen mode

Now to better organize our components export them in the ./components/index.js file

export { default as Navbar } from './Navbar';
export { default as Form } from './Form';
export { default as Post } from './Post';
Enter fullscreen mode Exit fullscreen mode

After that, we import our components to our main page ./pages/index.js

...
import { Navbar, Form, Post } from "@/components";
...
 return (
   <div className="bg-[#eff2f5] flex flex-col min-h-screen">
     <Navbar />
     <div className="pt-12 pb-6 flex flex-col items-center justify-center">
       <Form />
       <Post />
     </div>
   </div>
 )
}

Enter fullscreen mode Exit fullscreen mode

Thirdweb Integrations:

Now that we are done creating the base UI for our app. Let's start the Web3 integrations to be able to interact with our contract

Install the dependencies

npm i @thirdweb-dev/sdk @thirdweb-dev/react --save

Navigate to ./pages/_app.js and copy the following code into it

import Head from 'next/head';
import { ChainId, ThirdwebProvider } from '@thirdweb-dev/react';
import '@/styles/globals.css'


export default function App({ Component, pageProps }) {
 return (
   <ThirdwebProvider network={ChainId.Mumbai}>
     <Head>
       <title>Next3Blog</title>
     </Head>
     <Component {...pageProps} />
   </ThirdwebProvider>
 )
}

Enter fullscreen mode Exit fullscreen mode

Here we are wrapping the app with the Thirdweb provider, and setting the network to the Mumbai chain id.

Inside ./pages/index.js copy the following code

import { useMetamask, useAddress } from "@thirdweb-dev/react";
export default function App() {
 const connect = useMetamask();
 const address = useAddress();
Enter fullscreen mode Exit fullscreen mode

We import a couple of hooks provided by Thirdweb, we're gonna need a useMetamask to connect the app to Metamask wallet and useAddress that gets the connected address

Then we pass both hooks to the Navbar component and address to the Form and Post.

...
<div className="bg-[#eff2f5] flex flex-col min-h-screen">
 <Navbar
   connect={connect}
   address={address}
 />
 <div className="pt-12 pb-6 flex flex-col items-center justify-center">
   <Form
     address={address}
   />
   <Post
     address={address}
   />
 </div>
</div>
...

Enter fullscreen mode Exit fullscreen mode

Let's implement the connect wallet functionality in the Navbar

export default function Navbar({ connect, address }) {
 return (
   <div className="bg-white">
       ...
       { address ?
           <div className="border border-[#c7cacd] text-gray-600 px-2 py-1 rounded-[12px] inline-flex items-center">
               <div className="w-8 h-8 mr-2 rounded-full overflow-hidden border border-[#c7cacd] flex justify-center ">
                   <Identicon string={address} size={30}/>
               </div>
               <h2 className="font-openSans text-black text-[12px]">{truncateAddress(address)}</h2>
           </div> :
           <button className="group/item border border-[#c7cacd] text-gray-600 px-2 py-1 rounded-[12px] inline-flex items-center cursor-pointer hover:bg-[#1974E7] hover:text-white transition"
               onClick={() => connect()}
           >
               <h2 className='font-openSans group-hover/item:text-white text-[#1974E7] transition'>Connect Wallet</h2>
           </button>
       }
       ...
   </div>
 )
}

Enter fullscreen mode Exit fullscreen mode

Here we are doing conditional rendering, so we show the address if there is a connected address, or else show the connect button

Back to index.js

We import useContract to be able to connect to our contract and pass our smart contract address to it.

import { ..., useContract } from "@thirdweb-dev/react";
...
export default function App() {
...
const { contract } = useContract("0x1e55A7D8c854c239cD30EA737700D1C7d7273395");
Enter fullscreen mode Exit fullscreen mode

Now let's do our first contract call



...
 const [posts, setPosts] = useState([]);
...
const getPosts = async () => {
   try {
     const posts = await contract.call('getPosts');
     const parsedPosts = posts.map((post, i) => ({
       owner: post.owner,
       postContent: post.postContent,
       id: post.postId.toNumber()
     }));
     setPosts(parsedPosts);
   }
   catch (err) {
     console.error("contract call failure", err);
   }
 }

Enter fullscreen mode Exit fullscreen mode

Thirdweb makes it extremely simple to integrate our smart contract functions.
Here we are calling the getPosts function and then parsing through the returned posts array to get the values that we need. If you haven’t played around on Thirdweb dashboard and created a post, this should be returning an empty array.

Then we call the function inside useEffect to run whenever the component mounts

 useEffect(() => {
   if(contract) getPosts();
 }, [address, contract]);
Enter fullscreen mode Exit fullscreen mode

we check if the contract value exists before calling getPosts, and re-runs whenever the address or contract values is updated.

Next, we map over the posts array to return a list of Post components.

{posts.map(post =>
         <Post
           key={post.id}
           id={post.id}
           owner={post.owner}
           address={address}
           postContent={post.postContent}
         />)
       }
Enter fullscreen mode Exit fullscreen mode

Before we implement the create post functionality we need to keep track of the form input value, let’s declare a new useState variable and pass it down to the Form component

... 
const [text, setText] = useState('');
...
<Form
         address={address}
         setText={setText}
         text={text}
         buttonText={buttonText}
       />
...

Enter fullscreen mode Exit fullscreen mode

In the Form component we set up the event listener that is triggered whenever the user types something into the input field

export default function Form({ setText, text }) {
   return (
     <form className="lg:w-1/2 flex flex-col w-full h-full bg-white overflow-hidden mb-[50px] shadow-md rounded-[8px] py-5 px-10 box-border">
          ...


         <div className="w-4/5 lg:w-[90%]">
             <input type="text" id="post" className="font-openSans w-full p-3 text-md border rounded-[22px] focus:outline-none focus:shadow-outline" placeholder="Share Your Thoughts and Ideas!"
                 value={text}
                 onChange={e=>setText(e.target.value)}
             />
         </div>

         ...
      </form>
   )
 }

Enter fullscreen mode Exit fullscreen mode

Navigate back to index.js and let’s implement publishPost function

 const [publishLoading, setPublishLoading] = useState(false);
Enter fullscreen mode Exit fullscreen mode

We declare publishLoading state variable to track the loading state while executing the function

const publishPost = async (text) => {
   if(text.length > 0 && contract){
     try {
       setPublishLoading(true);
       const data = await contract.call('addPost', text);
       if(data.receipt.status === 1){
         getPosts();
         setText('');
       }
       return data;
     } catch (err) {
       console.error("contract call failure", err);
     } finally {
       setPublishLoading(false);
     }
   }
 }

Enter fullscreen mode Exit fullscreen mode

Here we are calling the addPost function and passing the input text to it, after checking if the text input is not empty and the contract exists.

If the transaction was successful, we call the getPosts function, and clear out the input field.

Next, we pass the function down to the Form component

...
<Form
         ...
         publishPost={publishPost}
         ...
       />
...
Enter fullscreen mode Exit fullscreen mode

Then we update the Form component to trigger the publishPost on submit

export default function Form({ ... text, publishPost }) {
   return (
     <form className="lg:w-1/2 flex flex-col w-full h-full bg-white overflow-hidden mb-[50px] shadow-md rounded-[8px] py-5 px-10 box-border" onSubmit={() => publishPost(text)}>
          ...
      </form>
   )
 }

Enter fullscreen mode Exit fullscreen mode

Now we implement the deletePost function

onst deletePost = async (id) => {
   try {
     const data = await contract.call('deletePost', id);
     if(data.receipt.status === 1){
       getPosts();
     }
     return data;
   } catch (err) {
     console.error("contract call failure", err);
   }

Enter fullscreen mode Exit fullscreen mode

Similar to the functions we implemented earlier, we call the deletePost function and passing the post id to it, and pass it down to Post component

{posts.map(post =>
         <Post
           ...
           deletePost={deletePost}
         />)
       }
Enter fullscreen mode Exit fullscreen mode

Update Post.js

export default function Post({ ... id, owner, address, deletePost }) {
 return (
   <div className="lg:w-1/2 flex flex-col w-full h-full bg-white overflow-hidden mb-6 shadow-md rounded-[8px] pt-2 pb-5 px-6 box-border">
               { owner === address &&
           <button className="w-[10px] cursor-pointer" onClick={() => deletePost(id)}>
             <svg className="fill-current w-3 h-3" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="122.878px" height="122.88px" viewBox="0 0 122.878 122.88" enableBackground="new 0 0 122.878 122.88" xml-space="preserve">
               <g>
                 <path d="M1.426,8.313c-1.901-1.901-1.901-4.984,0-6.886c1.901-1.902,4.984-1.902,6.886,0l53.127,53.127l53.127-53.127 c1.901-1.902,4.984-1.902,6.887,0c1.901,1.901,1.901,4.985,0,6.886L68.324,61.439l53.128,53.128c1.901,1.901,1.901,4.984,0,6.886 c-1.902,1.902-4.985,1.902-6.887,0L61.438,68.326L8.312,121.453c-1.901,1.902-4.984,1.902-6.886,0 c-1.901-1.901-1.901-4.984,0-6.886l53.127-53.128L1.426,8.313L1.426,8.313z"></path>
               </g>
             </svg>
           </button>
         }      


     </div>
 )
}

Enter fullscreen mode Exit fullscreen mode

Here we are checking if the owner of the post has the same address that is connected to the app, and if so showing the close button.

One final thing that is left, let's add some conditional styles and text to the submit button. We need to disable it if the app is not connected to the wallet or not connected to the Mumbai network, or if the publishLoading state variable is true.

index.js

... 
const buttonText = !address ? "Connect Wallet" : chainId !== ChainId.Mumbai ? "Connect to the Mumbai TestNet" : publishLoading ? "Loading..." : "Publish";
 const buttonDisabled = !address || chainId !== ChainId.Mumbai || publishLoading;
...

Enter fullscreen mode Exit fullscreen mode

Path both variables to Form component

<Form
        ...
         buttonDisabled={buttonDisabled}
         buttonText={buttonText}
       />
Enter fullscreen mode Exit fullscreen mode

Update Form.js

export default function Form({ ... buttonText, buttonDisabled }) {
 return (
   <form className="lg:w-1/2 flex flex-col w-full h-full bg-white overflow-hidden mb-[50px] shadow-md rounded-[8px] py-5 px-10 box-border" onSubmit={() => publishPost(text)}>


       ...


       <div className="w-full justify-center items-center">
       <button className="w-full bg-[#1974E7] px-2 py-2 rounded-[6px] flex justify-center cursor-pointer disabled:opacity-50 disabled:cursor-default" type='submit' disabled={buttonDisabled} onClick={() => publishPost(text)}>
           <h2 className='font-openSans text-white transition'>{buttonText}</h2>
       </button>
       </div>


   </form>
 )
}

Enter fullscreen mode Exit fullscreen mode

Notice how we are using the Tailwind's disabled: modifier to customize the disabled style.

And that's a wrap!

Full code can be found in the github repo here.

Conclusion:

Whew, you made it to the end of the tutorial! You now have a solid understanding of how to build a full-stack dApp using some of the coolest and trendiest technologies out there. To take your learning further, you can add the update functionality to the post content, loading state for getPosts, and you can also use packages like Draft.js to add a customizable text editor instead of the simple input that we implemented. Additionally, you can implement user profile routing.

Thanks for following along. Have a good one!

Latest comments (0)