Web3 Twitter application on React.js + Solidity

Hello, in the first part the project was prepared, wallets were connected and the backend was written in Solidity, which means it’s time to write the frontend in React.

The project is far from production and is a simple example for beginners, designed to demonstrate interaction with a smart contract through a web application.

Returning to the main project folder web3, in which we create a project using React.

$ npx create-react-app client

Next you need to install two libraries: react-router-domAndweb3. The first one is needed to navigate between pages, and the second one is needed to work with a smart contract.

cd client
npm i react-router-dom, web3

If you use yarn:

cd client
yarn add web3
yarn add react-router-dom

We see our libraries in dependencies, so we can start.

Client preparation

Let's start with removal extra files.

Delete

Delete

Go to the file index.js and import BrowserRouter from the library react-router-dom also wrap the component App to the router. We remove unnecessary comments in the code and clean up imports.

Next, create two folders: config And contexts. In folder config create two files: abi.json (it will contain the abi of our smart contract) And contract.js (it will contain the address of our smart contract).
PS: the contract address can be viewed in 1 block in Ganache .

ABI – in the context of smart contracts – is like a language of communication between various programs or software components.

In the case of smart contracts, the ABI is a set of rules and data formats that define how external programs can interact with these contracts. This includes the data transfer format, data types, and function structure.

In folder contexts create a file ContractContext.js in it we will describe connecting to the wallet MetaMask and smart contract, as well as return variables, in order to have access to them on any page of the site, simply by calling useContract.

In file contract.js there will be just one line:

export const contract_address = "адрес вашего смарт-контракта";

Now we need to find ABI our contract to paste it into the file abi.json. To do this, go to the folder contracts with our backend and open the folder build/contarcts and there is a file in it Twitter.json. In it we find abi and copy it completely.

Just paste the copied text into our file abi.json. Below abi for the contract from the first part.

Hidden text
[
    {
      "inputs": [],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_username",
          "type": "string"
        },
        {
          "internalType": "string",
          "name": "_password",
          "type": "string"
        }
      ],
      "name": "Registration",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_password",
          "type": "string"
        }
      ],
      "name": "Login",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "Logout",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_text",
          "type": "string"
        }
      ],
      "name": "AddTwitt",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_user",
          "type": "address"
        }
      ],
      "name": "UserTwitts",
      "outputs": [
        {
          "components": [
            {
              "components": [
                {
                  "internalType": "address",
                  "name": "login",
                  "type": "address"
                },
                {
                  "internalType": "string",
                  "name": "password",
                  "type": "string"
                },
                {
                  "internalType": "string",
                  "name": "username",
                  "type": "string"
                },
                {
                  "internalType": "string",
                  "name": "avatar",
                  "type": "string"
                }
              ],
              "internalType": "struct Twitter.User",
              "name": "author",
              "type": "tuple"
            },
            {
              "internalType": "string",
              "name": "text",
              "type": "string"
            },
            {
              "internalType": "uint256",
              "name": "likes",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "createdTime",
              "type": "uint256"
            }
          ],
          "internalType": "struct Twitter.Twitt[]",
          "name": "",
          "type": "tuple[]"
        }
      ],
      "stateMutability": "view",
      "type": "function",
      "constant": true
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_user",
          "type": "address"
        }
      ],
      "name": "CheckRegistration",
      "outputs": [
        {
          "internalType": "bool",
          "name": "",
          "type": "bool"
        }
      ],
      "stateMutability": "view",
      "type": "function",
      "constant": true
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_user",
          "type": "address"
        }
      ],
      "name": "GetUser",
      "outputs": [
        {
          "components": [
            {
              "internalType": "address",
              "name": "login",
              "type": "address"
            },
            {
              "internalType": "string",
              "name": "password",
              "type": "string"
            },
            {
              "internalType": "string",
              "name": "username",
              "type": "string"
            },
            {
              "internalType": "string",
              "name": "avatar",
              "type": "string"
            }
          ],
          "internalType": "struct Twitter.User",
          "name": "",
          "type": "tuple"
        }
      ],
      "stateMutability": "view",
      "type": "function",
      "constant": true
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_avatar",
          "type": "string"
        }
      ],
      "name": "UpdateUser",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ]

Next we write ContractProvider I hope you are at least a little familiar with js and react, but in any case the code will be simple.

First of all, we pull up the web3 library and our contract address along with abi.

Next we create the context ContractContext.

The context is used to pass data down the component tree without explicitly passing props.

In the component itself ContractProvider We declare several states.

  1. contract – contains an instance of a smart contract.

  2. account – contains the address of the user logged into the account.

  3. accounts – contains the addresses of all users.

  4. balance – contains the balance of the user logged into the account.

const [contract, setContract] = useState(null);
const [account, setAccount] = useState(null);
const [accounts, setAccounts] = useState([]);
const [balance, setBalance] = useState();

Let's move on to writing a function for connecting a user account to the application. In it we create an instance of the object web3 to interact with Ethereum. The connection goes through MetaMask or a local server with the address and port that we specified in Ganache.

If the extension MetaMask installed, then through window.ethereum.request we get all accounts and the first ([0]) is written down as the user's address.

By using web3.eth.getBalance(accounts[0]) we get the balance of the user's account and also write it to the balance state (before that we transfer from wei V ether) and take only the first 7 characters.

Below we create an instance of the contract and also write it to the state.

const contractInstance = new web3.eth.Contract(abi, contract_address);
setContract(contractInstance);

We wrap everything in try-catch for reliability and call the function inside UseEffectthus the function will be executed when the component is mounted.

Hidden text
useEffect(() => {
    const initializeContract = async () => {
      const web3 = new Web3(Web3.givenProvider || 'http://127.0.0.1:7545');
      if (window.ethereum) {
        try {
          const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
          if (accounts && accounts.length > 0) {
            setAccounts(accounts);
            setAccount(accounts[0]);
  
            const weiBalance = await web3.eth.getBalance(accounts[0]);
            const etherBalance = web3.utils.fromWei(weiBalance, 'ether');
            setBalance(etherBalance.slice(0, 8));

            const contractInstance = new web3.eth.Contract(abi, contract_address);
            setContract(contractInstance);
          } else {
            console.error('No accounts found');
          }
        } catch (error) {
          console.error('Error fetching account balance:', error);
        }
      } else {
        alert('Install MetaMask extension!');
      }
    };

    initializeContract();
  }, []);

We will return child components wrapped in ContractContext.Provider and our states with accounts, balance, etc.

return (
    <ContractContext.Provider value={{ contract, account, balance, accounts }}>
      {children}
    </ContractContext.Provider>
);

Full context code

Hidden text
// ContractContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
import Web3 from 'web3';
import { contract_address } from '../config/contract';
import abi from '../config/abi.json';

const ContractContext = createContext();

export const ContractProvider = ({ children }) => {
  const [contract, setContract] = useState(null);
  const [account, setAccount] = useState(null);
  const [accounts, setAccounts] = useState([]);
  const [balance, setBalance] = useState();

  useEffect(() => {
    const initializeContract = async () => {
      const web3 = new Web3(Web3.givenProvider || 'http://127.0.0.1:7545');
      if (window.ethereum) {
        try {
          const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
          if (accounts && accounts.length > 0) {
            setAccounts(accounts);
            setAccount(accounts[0]);
  
            const weiBalance = await web3.eth.getBalance(accounts[0]);
            const etherBalance = web3.utils.fromWei(weiBalance, 'ether');
            setBalance(etherBalance.slice(0, 8));

            const contractInstance = new web3.eth.Contract(abi, contract_address);
            setContract(contractInstance);
          } else {
            console.error('No accounts found');
          }
        } catch (error) {
          console.error('Error fetching account balance:', error);
        }
      } else {
        alert('Install MetaMask extension!');
      }
    };

    initializeContract();
  }, []);

  return (
    <ContractContext.Provider value={{ contract, account, balance, accounts }}>
      {children}
    </ContractContext.Provider>
  );
};

export const useContract = () => {
  const context = useContext(ContractContext);
  if (!context) {
    throw new Error('useContract must be used within a ContractProvider');
  }
  return context;
};

Don't forget to wrap router And app in our ContractProvider.

Layout of pages and creation of components

Next, by and large, there will be the usual layout and creation of components, the code for all css files will be at the end, I don’t see the point in focusing on the design.

First of all, let's create our own header, because The functionality is very limited, we press on it the name of the application and the profile icon to go to another page.

Create a folder components for our components and a folder with constants, we add the main color of the application to it.

IN CustomHeader simply adds a title for the title and an image for the button to go to the profile page. For the transition we use Link from react-router-dom.

export default function CustomHeader() {
  return (
    <div style={{
        height: '10vh', 
        backgroundColor: theme.primaryColor,
        display: 'flex', 
        alignItems: 'center',
        justifyContent: 'space-between',
        paddingInline: '10%'
      }}
      >

      <Link to={"/"}><h1 style={{color: 'white'}}>Twitter dApp</h1></Link>
      <Link to={"/profile"}>
        <img style={{
          width: '6vh', 
          height: '6vh'}} 
          src={process.env.PUBLIC_URL + '/userIcon.png'} 
          alt="user"/>
      </Link>
    </div>
  )
}

Let's also create a folder pages and it has 4 pages: home, profile, another user’s profile and a “404” page. Now they only have div with page title

Go to the file App.js and import our CustomHeader. Inside the function we add our CustomHeader And routes. We transfer our pages from the folder as page elements pages.

For the “404” page, specify “*” as the path. Now if the user changes the path to a non-existent one (any other than those we declared), he will be given an error page.

For another user's page, add the dynamic parameter “:userAdr” to the path so that the user's address can be passed in the request parameters and received on the page.

function App() {
  return (
    <div className="App">
      <CustomHeader/>

      <Routes>
        <Route path="/" element={<Home/>} />
        <Route path="/profile" element={<Profile/>} />
        <Route path="/user/:userAdr" element={<User/>} />
        <Route path="*" element={<ErrorPage/>} />
      </Routes>
    </div>
  );
}

On the page with the error, add a basic inscription and a button that returns to the main page (“/”).

return (
    <div className="App-header">
      <h1 style={{textAlign: 'center'}}>Oops...<br/>Page not found</h1>
      <Link to="/" style={{
        backgroundColor: '#3366FF', 
        padding: '10px',
        paddingInline: '20px', 
        borderRadius: '5px',
        color: 'white'}}>Go home</Link>
    </div>
)

Let's launch Ganache and client:

cd client
npm run start

After this we see our header and an open window MetaMask with connecting your account to the application. Select the first account and connect.

Also, if we change the page path to a non-existent one, we will see an error page. Everything works and all that remains is to add the component twitter and profile content.

In folder components create a file Twitt.jsx, as an argument, it will accept the tweet itself, which will be transmitted on the tweet output page.

From our context, we import only the account address; it will be useful for checking the author. Declare a constant isOwner, its value depends on whether the authorized user is the author of the tweet. We also convert the date into a user-friendly format.

const { account } = useContract();
const isOwner = account == twit.author.login.toLowerCase();

const date = new Date(Number(twit.createdTime) * 1000);
const formattedDateTime = date.toLocaleString();

Next we simply output to div field blocks from the transmitted tweet and wrap the author’s avatar and nickname in Linkso that when clicked, it goes to the user’s page.

If the authorized user is the author of the tweet, then the transition will not be to the author’s page, but to the profile. When going to the author’s page, we pass in the parameters his address, which is stored in twitt.

PS: It would be correct to make one profile page and distinguish between logic and design for the owner and the guest. But within this application we have divided the page into two.

import React from 'react'
import theme from '../constants/colors'
import './Twitt.css';
import { Link } from 'react-router-dom';
import { useContract } from '../contexts/ContractContext';

export default function Twitt({twit}) {
    const { account } = useContract();
    const isOwner = account == twit.author.login.toLowerCase();
    
    const date = new Date(Number(twit.createdTime) * 1000);
    const formattedDateTime = date.toLocaleString();

    return (
        <div className="Twitt">
            <Link style={{color: 'black'}} to={isOwner ? '/profile' : `/user/${twit.author.login}`}>
                <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '2vh'}}>
                    <img className="twitt-pic" src={twit.author.avatar ? twit.author.avatar : `${process.env.PUBLIC_URL}/profile-pic.png`} alt="Profile Picture"/>
                    <div>
                        <h3>@{twit.author.username}</h3>
                        <h3 style={{color: theme.primaryColor}}>{formattedDateTime}</h3>
                    </div>
                </div>
            </Link>
            <h3 style={{marginTop: '2vh'}}>{twit.text}</h3>
            <div className="likes">
                <img src={process.env.PUBLIC_URL+'/like.png'} className="like"/>
                <h3>{twit.likes.toString()}</h3>
            </div>
        </div>
    )
}

Let's go to the profile page, where we immediately announce several states. For convenience, I did not move the login forms and avatar updates into separate components or modal windows, so everything will be stored inside one page.

Here we import the user’s balance and address from the contract to display it in the profile, as well as the contract itself to work with it.

We also declare a state for the registration, login and tweet text fields and several boolean states. isOpened needed to track when the user's avatar change button is pressed (if the value is true a field for entering a link to the image will be displayed).

const { contract, balance, account } = useContract();
const [user, setUser] = useState();
const [link, setLink] = useState();

const [text, setText] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');

const [isOpened, setIsOpened] = useState(false);
const [isLogged, setIsLogged] = useState(false);
const [isRegistered, setIsRegistered] = useState(false);

const [twitts, setTwitts] = useState([]);

Next we announce useEffect, which will be updated when one of the three states changes (contract, registration or login). If the contract is initialized, it receives the authorization state from localStorage, we also call a function that checks whether the user is registered or not. If the user is authorized, we receive his data and tweets. The functions themselves will be described below.

useEffect(() => {
  if (contract) {
    const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
    setIsLogged(isLoggedIn);

    CheckRegistration();
    
    if(isLogged) {
      GetUserData();
      getUserTwitts();
    }
  }
}, [isLogged, contract, isRegistered]);

Let's create a universal function for working with a contract, in which we will receive in the parameters the name of the function in the smart contract and the arguments necessary for transmission.

Also using estimateGas We find out the cost of the transaction and pass it as the second argument. IN Ganache You can also specify the 0th transaction price and not specify gas when calling functions. All functions will be called from the user's address, so we indicate from: account.

const sendTransaction = async (method, args) => {
  try {
    const gasEstimation = await method(...args).estimateGas({ from: account });
    await method(...args).send({ from: account, gas: gasEstimation });
  } catch (err) {
    alert(err);
  }
};

Now we create functions for registration, authorization and writing a tweet in which we check that the passed arguments are not empty and pass them to the function sendTransactionas methodwe pass the name of the function in the smart contract itself.

Hidden text
const AddTwitt = async(_text) => {
  if(!_text) return;

  try {
    await sendTransaction(contract.methods.AddTwitt, [_text]);

    setText('');
    getUserTwitts();
  } catch (err) {
    alert(err);
  }
}

const Registration = async(_username, _password) => {
  if(!_username || !_password) return;
  try {
    await sendTransaction(contract.methods.Registration, [_username, _password]);

    setIsRegistered(true);
  } catch (err) {
    alert(err);
  }
}

const Login = async(_password) => {
  if(!_password) return;
  try {
    await sendTransaction(contract.methods.Login, [_password]);

    localStorage.setItem('isLoggedIn', true);
    setIsLogged(true);
  } catch (err) {
    alert(err);
  }
}

In the function with receiving data, we access the contract function via call, and we don’t pay for calling them, so we don’t pass on the cost of gas. We pass the user's address as an argument, because we get his data.

const getUserTwitts = async() => {
  try {
    const twitts = await contract.methods.UserTwitts(account).call({ from: account });

    setTwitts(twitts);
  } catch (err) {
    alert(err);
  }
}

const GetUserData = async() => {
  try {
    const user = await contract.methods.GetUser(account).call({ from: account });

    setUser(user);
  } catch (err) {
    alert(err);
  }
}

Next is the usual page layout, let's look at the main points.

First, we add the condition that if the user is not logged into the account and is not registered, then we display fields for registration, and upon successful registration or if the user already has an account, he will only have a field for entering a password (after all, the login is the account address).
If the user is already logged in, we display the logout button.

<div>
  {
    isLogged 
      ? (<button className="edit-btn" onClick={() => Exit()}>exit</button>)
      : (
          isRegistered
          ? (
            <div className="registration">
              <input type="text" placeholder="password" value={password} onChange={e => setPassword(e.target.value)}/>
              <button className="edit-btn" style={{margin: '0'}} onClick={() => Login(password)}>login</button>
            </div>
            )
          : (
            <div className="registration">
              <input type="text" placeholder="username" value={username} onChange={e => setUsername(e.target.value)}/>
              <input type="text" placeholder="password" value={password} onChange={e => setPassword(e.target.value)}/>
              <button className="edit-btn" style={{margin: '0'}} onClick={() => Registration(username, password)}>registration</button>
            </div>
            )
        )
  }
</div>

We also display an avatar update button in the side menu; when clicked, a text field for entering a link and a new button will appear.

<div className="sidebar">
  {user && <h2 style={{marginBottom: '4vh'}}>
    <span style={{color: 'gray'}}>Username:</span><br/>@{user.username}
  </h2>}
  {isOpened && <input type="text" style={{marginBottom: '1vh'}} placeholder="link to img" value={link} onChange={e => setLink(e.target.value)}/>}
  {isOpened 
    ? <button className="edit-btn" onClick={() => UpdateAvatar(link)}>save</button>
    : <button className="edit-btn" onClick={() => setIsOpened(true)}>update avatar</button>
  }
</div>

To display all tweets via map we go through the array of tweets and pass each tweet to our component Twittwe pass the date of creation of the tweet as a key, and also sort them by time so that new ones are displayed first.

Better

{
  twitts.sort((a, b) => Number(b.createdTime) - Number(a.createdTime)).map((twit) => {
    return (
      <Twitt key={twit.createdTime} twit={twit} />
    );
  })
}

Full profile page code

Hidden text
import React from 'react'
import '../App.css';
import { useEffect, useState } from 'react';
import './Profile.css';
import theme from '../constants/colors';
import { useContract } from '../contexts/ContractContext';
import Twitt from '../components/Twitt';

export default function Profile() {
    const { contract, balance, account } = useContract();
    const [user, setUser] = useState();
    const [link, setLink] = useState();

    const [text, setText] = useState('');
    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');

    const [isOpened, setIsOpened] = useState(false);
    const [isLogged, setIsLogged] = useState(false);
    const [isRegistered, setIsRegistered] = useState(false);
    
    const [twitts, setTwitts] = useState([]);

    useEffect(() => {
      if (contract) {
        const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
        setIsLogged(isLoggedIn);

        CheckRegistration();
        
        if(isLogged) {
          GetUserData();
          getUserTwitts();
        }
      }
    }, [isLogged, contract, isRegistered]);

    const sendTransaction = async (method, args) => {
      try {
        const gasEstimation = await method(...args).estimateGas({ from: account });
        await method(...args).send({ from: account, gas: gasEstimation });
      } catch (err) {
        alert(err);
      }
    };

    const AddTwitt = async(_text) => {
      if(!_text) return;

      try {
        await sendTransaction(contract.methods.AddTwitt, [_text]);

        setText('');
        getUserTwitts();
      } catch (err) {
        alert(err);
      }
    }

    const Registration = async(_username, _password) => {
      if(!_username || !_password) return;
      try {
        await sendTransaction(contract.methods.Registration, [_username, _password]);

        setIsRegistered(true);
      } catch (err) {
        alert(err);
      }
    }

    const Login = async(_password) => {
      if(!_password) return;
      try {
        await sendTransaction(contract.methods.Login, [_password]);

        localStorage.setItem('isLoggedIn', true);
        setIsLogged(true);
      } catch (err) {
        alert(err);
      }
    }

    const getUserTwitts = async() => {
      try {
        const twitts = await contract.methods.UserTwitts(account).call({ from: account });

        setTwitts(twitts);
      } catch (err) {
        alert(err);
      }
    }

    const GetUserData = async() => {
      try {
        const user = await contract.methods.GetUser(account).call({ from: account });

        setUser(user);
      } catch (err) {
        alert(err);
      }
    }

    const UpdateAvatar = async(link) => {
      if(!link) return;
      try {
        await sendTransaction(contract.methods.UpdateUser, https://habr.com/ru/articles/799819/);

        setLink('');
        setIsOpened(false);
        GetUserData();
      } catch (err) {
        alert(err);
      }
    }

    const Exit = async() => {
      try {
        await sendTransaction(contract.methods.Logout, []);

        localStorage.setItem('isLoggedIn', false);
        setIsLogged(false);
        setTwitts([]);
      } catch (err) {
        alert(err);
      }
    }

    const CheckRegistration = async() => {
      try {
        const result = await contract.methods.CheckRegistration(account).call({ from: account });
        setIsRegistered(result);
      } catch (err) {
        alert(err);
      }
    }

    return (
      <div className="main">
        <div className="container" 
          style={{
            alignItems: 'start', 
            justifyContent: 'flex-start',
            gap: '5vh'}}>
          <div>
            <img src={user && user.avatar ? user.avatar : process.env.PUBLIC_URL + '/basicProfile.png'} alt="avatar" className="avatar" />
          </div>
          <div className="user-info">
            <div style={{marginTop: '5vh'}}>
              <h2>Account: <span style={{color: theme.primaryColor}}>{account}</span></h2>
              <h2>Balance: {balance} BEBRA</h2>
            </div>
            {
              isLogged 
                ? (<button className="edit-btn" onClick={() => Exit()}>exit</button>)
                : (
                    isRegistered
                    ? (
                      <div className="registration">
                        <input type="text" placeholder="password" value={password} onChange={e => setPassword(e.target.value)}/>
                        <button className="edit-btn" style={{margin: '0'}} onClick={() => Login(password)}>login</button>
                      </div>
                      )
                    : (
                      <div className="registration">
                        <input type="text" placeholder="username" value={username} onChange={e => setUsername(e.target.value)}/>
                        <input type="text" placeholder="password" value={password} onChange={e => setPassword(e.target.value)}/>
                        <button className="edit-btn" style={{margin: '0'}} onClick={() => Registration(username, password)}>registration</button>
                      </div>
                      )
                  )
            }
          </div>
        </div>
        
        <div className="container" style={{
            alignItems: 'start', 
            justifyContent: 'flex-start', 
            marginBottom: '5vh',
            gap: '5vh'
            }}>
          
          {isLogged && <div className="sidebar">
            {user && <h2 style={{marginBottom: '4vh'}}>
              <span style={{color: 'gray'}}>Username:</span><br/>@{user.username}
            </h2>}
            {isOpened && <input type="text" style={{marginBottom: '1vh'}} placeholder="link to img" value={link} onChange={e => setLink(e.target.value)}/>}
            {isOpened 
              ? <button className="edit-btn" onClick={() => UpdateAvatar(link)}>save</button>
              : <button className="edit-btn" onClick={() => setIsOpened(true)}>update avatar</button>
            }
          </div>}
          <div style={{display: 'flex', flexDirection: 'column', gap: '5vh', width: '80%'}}>
            {isLogged && <div className="add-twitt">
              <textarea className="input_twitt" type="text" placeholder="What are you thinking?" value={text} onChange={e => setText(e.target.value)}/>
              <button onClick={() => AddTwitt(text)} disabled={!isLogged} className="add-btn">Twitt it</button>
            </div>}

            {
              twitts.sort((a, b) => Number(b.createdTime) - Number(a.createdTime)).map((twit) => {
                return (
                  <Twitt key={twit.createdTime} twit={twit} />
                );
              })
            }
          </div>
        </div> 
      </div>
    )
}

We go to the profile page and see the fields for registration. Let's create an account and try to log into it.

After logging into our account, we see all our blocks, we’ll try to update the photo, insert a link to the picture from the browser, and also write a tweet.

After successfully writing a tweet, we immediately see it in the feed, we also see our new picture (you can also put a GIF).

Home page

Now all that’s left to do is make a main page with all the tweets of other users. To do this, we also create a state for tweets and import all accounts from the context, along with the user’s account.

Create a function to get all tweets. In it, we go through all the accounts and get tweets from the user whose address is in the current iteration. We enter all tweets into a set in order to store only unique tweets, and after the cycle is completed, we enter the tweets into the state.

Not the best example of retrieving data, and by and large it would be worth writing a function on the server that will return all tweets. But within the framework of this project, for ease of development, we will leave this solution.

const getAllTwitts = async() => {
  try {
    const uniqueTwittsSet = new Set();

    for (const acc of accounts) {
      if(acc != account) {
        const _twitts = await contract.methods.UserTwitts(acc).call({ from: account });
      
        _twitts.forEach((twit) => uniqueTwittsSet.add(twit));
      }
    };

    const uniqueTwittsArray = [...uniqueTwittsSet];

    setTwitts(uniqueTwittsArray);
  } catch (err) {
    alert(err);
  }
}

Further also through mapdisplay tweets.

import React, {useEffect, useState} from 'react'
import { useContract } from '../contexts/ContractContext';
import Twitt from '../components/Twitt';

export default function Home() {
  const { contract, accounts, account } = useContract();
  const [twitts, setTwitts] = useState([]);

  const getAllTwitts = async() => {
    try {
      const uniqueTwittsSet = new Set();

      for (const acc of accounts) {
        if(acc != account) {
          const _twitts = await contract.methods.UserTwitts(acc).call({ from: account });
        
          _twitts.forEach((twit) => uniqueTwittsSet.add(twit));
        }
      };

      const uniqueTwittsArray = [...uniqueTwittsSet];

      setTwitts(uniqueTwittsArray);
    } catch (err) {
      alert(err);
    }
  }

  useEffect(() => {
    if (contract) {
      getAllTwitts();
    }
  }, [contract]);

  return (
    <div className="main">
      <div className="container" style={{display: 'flex', justifyContent: 'center', marginBottom: '5vh'}}>
        <div style={{display: 'flex', flexDirection: 'column', gap: '5vh', width: '80%'}}>
          {
            twitts.sort((a, b) => Number(b.createdTime) - Number(a.createdTime)).map((twit) => {
              return (
                <Twitt key={twit.createdTime}  twit={twit} />
              );
            })
          }
        </div>
      </div>
    </div>
  )
}

Let's log out of the account in the profile and switch the account to MetaMaskwe will also go through the registration stage for a new account and write a tweet.

Switching account

Switching account

And now on the main page we see the tweet of the first user, because… logged into the second account.

If we click on the user’s nickname or picture, we will go to his page and see his address in the link to the page; all that remains is to copy the layout of the profile page and remove all unnecessary things.

Of all the functions and elements of the page, we leave only receiving user data and his tweets. Here, as an argument, we pass the user’s address obtained from the page parameters (we specified it in routing as userAdr).

PS: I repeat that you should not do this and it would be correct to make one page for the authorized user and other users.

import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom';
import '../App.css';
import './Profile.css';
import theme from '../constants/colors';
import { useContract } from '../contexts/ContractContext';
import Twitt from '../components/Twitt';

export default function User() {
    const { contract, account } = useContract();
    const { userAdr } = useParams();
    const [user, setUser] = useState();
    const [twitts, setTwitts] = useState([]);

    useEffect(() => {
      if (contract) {
          GetUserData();
          getUserTwitts();
      }
    }, [contract]);

    const getUserTwitts = async() => {
      try {
        const twitts = await contract.methods.UserTwitts(userAdr).call({ from: account });

        setTwitts(twitts);
      } catch (err) {
        alert(err);
      }
    }

    const GetUserData = async() => {
      try {
        const _user = await contract.methods.GetUser(userAdr).call({ from: account });

        setUser(_user);
      } catch (err) {
        alert(err);
      }
    }

    return (
      <div className="main">
        <div className="container" 
          style={{
            alignItems: 'start', 
            justifyContent: 'flex-start',
            gap: '5vh'}}>
          <div>
            <img src={user && user.avatar ? user.avatar : process.env.PUBLIC_URL + '/basicProfile.png'} alt="avatar" className="avatar" />
          </div>
          <div className="user-info">
            <div style={{marginTop: '5vh'}}>
              <h2>Account: <span style={{color: theme.primaryColor}}>{userAdr}</span></h2>
            </div>
          </div>
        </div>
        
        <div className="container" style={{
            alignItems: 'start', 
            justifyContent: 'flex-start', 
            marginBottom: '5vh',
            gap: '5vh'
            }}>
          
          <div className="sidebar">
            {user && <h2 style={{marginBottom: '4vh'}}>
              <span style={{color: 'gray'}}>Username:</span><br/>@{user.username}
            </h2>}
          </div>
          <div style={{display: 'flex', flexDirection: 'column', gap: '5vh', width: '80%'}}>
            {
              twitts.sort((a, b) => Number(b.createdTime) - Number(a.createdTime)).map((twit) => {
                return (
                  <Twitt key={twit.createdTime} twit={twit} />
                );
              })
            }
          </div>
        </div> 
      </div>
    )
}
Another user's page

Another user's page

At this point the client part is ready. Below is the code for everyone css files.

Twitt.css

Hidden text
.Twitt {
    width: calc(100%-2vh);
    height: auto;
    padding: 2vh;
    padding-top: 0.5vh;
    border-color: var(--primary-color);
    border-style: groove;
    border-width: 2px;
    border-radius: 2vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: start;
}

h3, h2 {
    text-align: left;
    margin: 0;
    padding: 0;
}

.likes {
    margin-top: 4vh;
    display: flex;
    flex-direction: row;
    gap: 1vh;
    align-items: center;
}

.like {
    width: 4vh;
    height: 4vh;
}

.twitt-pic {
    width: 6vh;
    height: 6vh;
}

Profile.css

Hidden text
.avatar {
    height: 30vh;
    width: 30vh;
    border-radius: 3vh;
}

.user-info {
    height: 30vh;
    display: flex;
    text-align: left;
    flex-direction: column;
    justify-content: space-between;
}

.edit-btn {
    cursor: pointer;
    margin-bottom: 5vh;
    min-width: 20vh;
    width: auto;
    max-width: 30vh;
    height: 5vh;
    color: white;
    font-size: 1.2rem;
    background-color: var(--primary-color);
    border-radius: 2vh;
    border-style: none;
}

.registration {
    display: flex;
    align-items: center;
    gap: 1.5vh;
}

.sidebar {
    height: auto;
    width: 30vh; 
    display: flex;
    flex-direction: column; 
    justify-content: left;
}

input {
    padding-inline: 5px;
    outline: none;
    height: 5vh;
    border-style: inset;
    border-color: var(--primary-color);
    border-radius: 2vh;
}

.add-twitt {
    padding: 1vh;
    border-style: groove;
    border-width: 2px;
    border-radius: 10px;
    border-color: black;
    height: 15vh;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
}

.input_twitt {
    outline: none;
    border-style: none;
    width: 90%;
    word-wrap: break-word;
    resize: none;
}

.add-btn {
    place-self: center;
    cursor: pointer;
    width: 15vh;
    height: 5vh;
    color: white;
    font-size: 1.2rem;
    background-color: var(--primary-color);
    border-radius: 1vh;
    border-style: none;
}

App.css

Hidden text
.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: white;
  min-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: #282c34;
}

.App-link {
  color: #61dafb;
}

index.css

Hidden text
:root {
  --primary-color: #3366ff;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

a {
  text-decoration: none;
}

h2 {
  margin: 0;
}

.main {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.container {
  width: 80%;
  margin-top: 5vh;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

Thank you for your time, I hope the article was useful and understandable.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *