A Web3 Full Stack To-do list Dapp with Notifications

A Web3 Full Stack To-do list Dapp with Notifications

Web3 For Beginners

·

7 min read

This is the simple implementation of a To-do list that includes - deploying your smart contract, making frontend using React, chakra-ui and also integrating Push Protocol to receive notifications for incomplete tasks.

At the end of the tutorial, this is how the notifications will look like-

The notification tells you that you have 2 pending tasks! Let's learn how we can implement this:

Starting with smart contract:

  1. First step is to initialize an npm project and install Hardhat

     npm init 
    
     //Now we can install Hardhat:
    
     npm install --save-dev hardhat
    
     //In the same directory where you installed Hardhat run:
    
     npx hardhat
    
  2. Select Create a Javascript hardhat.config.js with your keyboard and hit enter.

  3. You also need some additional plugins:

     npm install --save-dev @nomicfoundation/hardhat-toolbox
    
  4. Let’s start with Solidity code:

    You’ll need 3 functions:

    GetTodos - to get all the active todo,

    addTodo, - to add your task,

    RemoveTodo - deleting a block is expensive, so we’ll just use a variable to set if the task is deleted or not, and if it’s deleted we’ll not fetch it.

    Full solidity code with explanation (follow the comments for explanation) -

    ```jsx // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity 0.8.18;

    contract TaskContract { //events we need to emit: //emitting while adding task event AddTask(address recipient, uint taskId); //emitting while removing task event RemoveTask(uint taskId, bool isDeleted);

    //a task's content: struct Task{ uint id; //an unique id for each task string content; //what does the task day bool isDeleted; //delete the task when done } Task[] private tasks; //an array for tasks, private cause we only want the user of the task to see it mapping(uint256 => address) private userTasks; //mapping id of the task to the particular user

//function to add task: need to add the content and set the isDeleted as false since we just added the task function addTask(string memory _content, bool isDeleted) external { uint taskId = tasks.length; //so the id is unique as the length will change everytime we add a new task tasks.push(Task(taskId, _content, isDeleted)); //pushing in the array of task userTasks[taskId] = msg.sender; //mapping id of the task to the user's walltet address emit AddTask(msg.sender, taskId); //emitting the task }

function getTasks() external view returns (Task[] memory) { Task[] memory temporary = new Task; //making a temporay array uint counter = 0;

//to get the tasks of the particular user from the task pool of everyon for(uint i=0; i<tasks.length; i++){ if(userTasks[i]==msg.sender && tasks[i].isDeleted == false){ temporary[counter] = tasks[i]; counter++; } }

Task[] memory result = new Task;

//iterating through the task of the particular user for(uint i=0; i<counter; i++){ result[i] = temporary[i]; } return result; }

//deleting by setting isDeleted true function deleteTask(uint _taskId, bool _isDeleted) external { if(userTasks[_taskId] == msg.sender){ tasks[_taskId].isDeleted = _isDeleted; emit RemoveTask(_taskId, _isDeleted); } } }


5. Deploying the contract -

    5.1 This is what the deploy.js looks like

    ```jsx
    const hre = require("hardhat");

    async function main() {
    //get the contract
      const TaskContract = await hre.ethers.getContractFactory("TaskContract");
    //deploy it
      const taskContract = await TaskContract.deploy();

      await taskContract.deployed();
    //print the addresss
      console.log("TaskContract deployed to:", taskContract.address);

    }
    // We recommend this pattern to be able to use async/await everywhere
    // and properly handle errors.
    main().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });

5.2 make sure your hardhat.config.js file is perfectly configured-

    require("@nomicfoundation/hardhat-toolbox");
    require('dotenv').config();
    /** @type import('hardhat/config').HardhatUserConfig */
    console.log(process.env.ALCHEMY_URL);

    module.exports = {
      solidity: "0.8.18",
      networks: {
        goerli : {
         url: process.env.ALCHEMY_URL, 
          accounts: [process.env.PRIVATE_KEY]
        }
      }
    };

5.3 And your .env file;

ALCHEMY_URL ='',
PRIVATE-KEY =''

Now coming to front end.

There will be 3 steps:

  1. Integrating with smart-contract

  2. Displaying on frontend

  3. Showing it as notifications

Step 1: Integrating with frontend:

import React, {useState, useEffect} from 'react';
import './App.css';
import { Input, Button } from '@chakra-ui/react'
import {TaskContractAddress} from './config'

//don't forget to paste your abi in the utils 

import TaskAbi from './utils/TaskContract.json'
import {ethers} from 'ethers'
import Task from './Task';

function App() {
  const [tasks, setTasks] = useState([]); //all the tasks that will be fetched from smart contract
  const [input, setInput] = useState(''); //setting the input of the task
  const [currentAccount, setCurrentAccount] = useState(''); //your wallet address

//connecting the wallet
  const connectWallet = async() => {
    try{
        const {ethereum} = window;
        if(!ethereum){ 
          alert("Please install metamask")
          return
        }
        const accounts = await ethereum.request({method: 'eth_requestAccounts'});
        console.log("Connected", accounts[0]);
        setCurrentAccount(accounts[0]);
    }catch(error){
      console.log(error)
    }
  }

//to fetch all the tasks

  const getTasks = async() => { 
    try{
      const {ethereum} = window;
      if(ethereum){ 

        //get the provider & signer to sign the transaction
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();

        //get the contract
        const TaskContract = new ethers.Contract(TaskContractAddress, TaskAbi.abi, signer);

        //get all the tasks using the contract. if you remember we don't need to pass any arguments here
        let tasks = await TaskContract.getTasks();
        setTasks(tasks)
      }

    }catch(error){
      console.log(error)
    }
  }

//to create any task

  const createTask = async() => { 
    let task = {
      'content': input,
      'completed': false
    }
    try {
      const {ethereum} = window;
      if(ethereum){ 
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const TaskContract = new ethers.Contract(TaskContractAddress, TaskAbi.abi, signer);


        //remember we need to pass the content and the boolean value if it's deleted or nor
        await TaskContract.addTask(task.content, task.completed)
        .then((res) => {
          setTasks([...tasks, task]) //once added, get the value of the task and update the state of task
          console.log("Task added")
        }).catch((err) => {
          console.log(err) //incase of error
         })
      }
    } catch (error) {
      console.log(error)
    }

    setInput(''); // empty the input
  }


//to delete task we send the boolean value isDeleted as true. as deleting is expensive on the blocks
const deleteTask = async(key) => { 

    try {
      const {ethereum} = window;
      if(ethereum){ 
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const TaskContract = new ethers.Contract(TaskContractAddress, TaskAbi.abi, signer);
       //with the id, we pass isDeleted as true
       let deleteTx = await TaskContract.deleteTask(key, true) 
        //now we again fetch the new set of tasks without the deleted one
       let allTasks = await TaskContract.getTasks()
        setTasks(allTasks) //update the state of tasks with the new tasks
      }
    } catch (error) {
      console.log(error)
    }

  }

//fetching all tasks only on reload
  useEffect(() => {
    getTasks() 

  },[]);

}

Step 2: Displaying on frontend

//this is an easy task, where we simply show the task state
return (
    <div className="App">
//only when the wallet is connected
     {currentAccount ? (
      <div>
       <div>
            //value of input is input which we're passing during addTask 
          <Input placeholder="Enter Task" className='addTask' htmlSize={50} width='auto' value={input} onChange={e => setInput(e.target.value)} />
          <Button onClick={createTask}>Add Task</Button>
       </div>
        <div>
            //mapping through the task state since it's an array
          {tasks.map(task => 
            <Task 
              key={task.id}
              content={task.content}
              onClick={() => deleteTask(task.id)}
            />

            )}


        </div>
        </div>
     ) : (
    //if wallet is not connected
        <Button onClick={connectWallet}>Connect Wallet</Button>
     )}
    </div>
  );
}

export default App;

Making the tasks passed look beautiful by giving it some CSS:

import React from 'react'
import './App.css'
import {DeleteIcon} from '@chakra-ui/icons'
import{ Center, List,
  ListItem,} from '@chakra-ui/react'
const Task = ({content, onClick}) => {
  return (

    <List className='App'>
      <ListItem onClick={onClick} border='1px' width='500px' borderColor='gray.200'>{content}
      <DeleteIcon />
      </ListItem>
    </List>
  )
}

export default Task

Step 3:

The most exciting part- sending it as notifications:

  1. Go on https://staging.push.org

  2. Click on Create Channel, and learn more about it here

Here’s what the top will look like:

First you need to install the Push SDK and ethers:

npm install @pushprotocol/restapi ethers
import * as PushAPI from "@pushprotocol/restapi"; 
const PK = process.env.REACT_APP_PRIVATE_KEY; // channel private key
console.log(process.env.REACT_APP_PRIVATE_KEY)
const Pkey = `0x${PK}`;
const _signer = new ethers.Wallet(Pkey);

const sendNotification = async() => {

    try {
        //mapping through the tasks array
      for(let i=0; i<tasks.length; i++){

        //passing it in the api
      const apiResponse = await PushAPI.payloads.sendNotification({

        signer: _signer,
        type: 3, // broadcast
        identityType: 2, // direct payload
        notification: {
          title: `To-do list updated`,
          body: `Complete your Todo`
        },
        payload: {

          title: `${tasks.length} are pending`, //your title
          body: `${tasks.map(task => task.content)}`, //content
          cta: '', //you can ass any link, I am leaving it blank
          img: '' //any image to
        },
        recipients: `eip155:5:${currentAccount}`, //gonna be my account as I am sending a notification to myself
        channel: 'eip155:5:0x6F7919412318E65109c5698bd0E640fc33DE2337', // your channel address 
      //  console.log(tasks[i].content)
        env: 'staging' //on staging using goerly
      })
    }
    }catch (err) {
      console.error('Error: ', err);
    }
  }

//I have set the interval for 5 sec here, to test it but you can configure yours once every 2 hours or however you like it.
setInterval(sendNotification,5000)

Check your Push Browser Extention or Web App to see notifications! It's as easy as that!

Thanks for following the tutorial. Please let me know your comments below. See you soon!✨