Examples
Todo Manager

Todo Manager

How to build the simplest todo dapp with WeaveDB and Next.js (opens in a new tab).

Deploy a Database

Using the web console (opens in a new tab), follow the instructions on Deploying a Database

Configure Database

To keep things simple, we will only use one collection called tasks

Each document in the tasks collection must contain the following 4 fields: task, date, user_address, and done

Create a Collection

Using the web console (opens in a new tab), follow the instructions on Creating a Collection

Set up Data Schema

{
  type: "object",
  required: ["task", "date", "user_address", "done"],
  properties: {
    task: {
      type: "string",
    },
    user_address: {
      type: "string",
    },
    date: {
      type: "number",
    },
    done: {
      type: "boolean",
    },
  },
}

In this example, we are defining a data schema for the tasks collection.

Copy & paste the schema object above. Then, follow the instructions on Set up Data Schema using the web console (opens in a new tab)

Set up Access Control Rules

{
  "allow create": {
    and: [
      {
        "==": [
          { var: "request.auth.signer" },
          { var: "resource.newData.user_address" },
        ],
      },
      {
        "==": [
          { var: "request.block.timestamp" },
          { var: "resource.newData.date" },
        ],
      },
      {
        "==": [{ var: "resource.newData.done" }, false],
      },
    ],
  },
  "allow update": {
    and: [
      {
        "==": [
          { var: "request.auth.signer" },
          { var: "resource.newData.user_address" },
        ],
      },
      {
        "==": [{ var: "resource.newData.done" }, true],
      },
    ],
  },
  "allow delete": {
    "==": [
      { var: "request.auth.signer" },
      { var: "resource.data.user_address" },
    ],
  },
}
  • user_address must be set signer
  • date must be the block.timestamp
  • done must default to false
  • Only done can be updated to true by the task owner (user_address)
  • Only the task owner (user_address) can delete the task

Copy & paste the access control rules object above. Then, follow the instructions on Set up Access Control Rules using the web console (opens in a new tab)

Integrate with Frontend

Create NextJS Project

Set up a NextJS project with the app name todos.

yarn create next-app todos
cd todos
yarn dev
TypeScript? No
ESLint? No
Tailwind CSS? No
`src/` directory? No
App Router? No 
import alias? No

Now your dapp should be running at http://localhost:3000 (opens in a new tab).

For simplicity, we will write everything in one file at /pages/index.js.

Install Dependencies

Open a new terminal and move to the project root directory to continue depelopment.

We use these minimum dependencies.

yarn add ramda localforage weavedb-sdk ethers@5.7.2 @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6

The Complete Code

Open /pages/index.js and replace everything.

💡

Change the string value "WEAVEDB_CONTRACT_TX_ID" to the contractTxId you was given when you Deploy a Database

/pages/index.js
import { useState, useEffect } from "react"
import lf from "localforage"
import { isNil, map } from "ramda"
import SDK from "weavedb-sdk"
import { ethers } from "ethers"
import { Box, Flex, Input, ChakraProvider } from "@chakra-ui/react"
 
const contractTxId = WEAVEDB_CONTRACT_TX_ID
 
export default function App() {
  const [user, setUser] = useState(null)
  const [tasks, setTasks] = useState([])
  const [tab, setTab] = useState("All")
  const [initDB, setInitDB] = useState(false)
  const tabs = isNil(user) ? ["All"] : ["All", "Yours"]
  const [db, setDb] = useState(null)
 
  const setupWeaveDB = async () => {
    const _db = new SDK({
      contractTxId,
    })
    await _db.init()
    setDb(_db)
    setInitDB(true)
  }
  const getTasks = async () => {
    setTasks(await db.cget("tasks", ["date", "desc"]))
  }
 
  const getMyTasks = async () => {
    setTasks(
      await db.cget(
        "tasks",
        ["user_address", "==", user.wallet.toLowerCase()],
        ["date", "desc"]
      )
    )
  }
 
  const addTask = async (task) => {
    await db.add(
      {
        task,
        date: db.ts(),
        user_address: db.signer(),
        done: false,
      },
      "tasks",
      user
    )
    await getTasks()
  }
 
  const completeTask = async (id) => {
    await db.update(
      {
        done: true,
      },
      "tasks",
      id,
      user
    )
    await getTasks()
  }
 
  const deleteTask = async (id) => {
    await db.delete("tasks", id, user)
    await getTasks()
  }
 
  const login = async () => {
    const provider = new ethers.providers.Web3Provider(window.ethereum, "any")
    await provider.send("eth_requestAccounts", [])
    const wallet_address = await provider.getSigner().getAddress()
    let identity = await lf.getItem(
      `temp_address:${contractTxId}:${wallet_address}`
    )
    let tx
    let err
    if (isNil(identity)) {
      ;({ tx, identity, err } = await db.createTempAddress(wallet_address))
      const linked = await db.getAddressLink(identity.address)
      if (isNil(linked)) {
        alert("something went wrong")
        return
      }
    } else {
      await lf.setItem("temp_address:current", wallet_address)
      setUser({
        wallet: wallet_address,
        privateKey: identity.privateKey,
      })
      return
    }
    if (!isNil(tx) && isNil(tx.err)) {
      identity.tx = tx
      identity.linked_address = wallet_address
      await lf.setItem("temp_address:current", wallet_address)
      await lf.setItem(
        `temp_address:${contractTxId}:${wallet_address}`,
        JSON.parse(JSON.stringify(identity))
      )
      setUser({
        wallet: wallet_address,
        privateKey: identity.privateKey,
      })
    }
  }
 
  const logout = async () => {
    if (confirm("Would you like to sign out?")) {
      await lf.removeItem("temp_address:current")
      setUser(null, "temp_current")
    }
  }
 
  const checkUser = async () => {
    const wallet_address = await lf.getItem(`temp_address:current`)
    if (!isNil(wallet_address)) {
      const identity = await lf.getItem(
        `temp_address:${contractTxId}:${wallet_address}`
      )
      if (!isNil(identity))
        setUser({
          wallet: wallet_address,
          privateKey: identity.privateKey,
        })
    }
  }
 
  useEffect(() => {
    checkUser()
    setupWeaveDB()
  }, [])
 
  useEffect(() => {
    if (initDB) {
      if (tab === "All") {
        getTasks()
      } else {
        getMyTasks()
      }
    }
  }, [tab, initDB])
 
  const NavBar = () => (
    <Flex p={3} position="fixed" w="100%" sx={{ top: 0, left: 0 }}>
      <Box flex={1} />
      <Flex
        bg="#111"
        color="white"
        py={2}
        px={6}
        sx={{
          borderRadius: "5px",
          cursor: "pointer",
          ":hover": { opacity: 0.75 },
        }}
      >
        {!isNil(user) ? (
          <Box onClick={() => logout()}>{user.wallet.slice(0, 7)}</Box>
        ) : (
          <Box onClick={() => login()}>Connect Wallet</Box>
        )}
      </Flex>
    </Flex>
  )
 
  const Tabs = () => (
    <Flex justify="center" style={{ display: "flex" }} mb={4}>
      {map((v) => (
        <Box
          key={v}
          mx={2}
          onClick={() => setTab(v)}
          color={tab === v ? "red" : ""}
          textDecoration={tab === v ? "underline" : ""}
          sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
        >
          {v}
        </Box>
      ))(tabs)}
    </Flex>
  )
 
  const Tasks = () =>
    map((v) => (
      <Flex
        key={v.id}
        sx={{ border: "1px solid #ddd", borderRadius: "5px" }}
        p={3}
        my={1}
      >
        <Box
          w="30px"
          textAlign="center"
          sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
        >
          {v.data.done ? (
            "✅"
          ) : v.data.user_address !== user?.wallet.toLowerCase() ? null : (
            <Box onClick={() => completeTask(v.id)}>⬜</Box>
          )}
        </Box>
        <Box px={3} flex={1} style={{ marginLeft: "10px" }}>
          {v.data.task}
        </Box>
        <Box w="100px" textAlign="center" style={{ marginLeft: "10px" }}>
          {v.data.user_address.slice(0, 7)}
        </Box>
        <Box
          w="50px"
          textAlign="center"
          sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
        >
          {v.data.user_address === user?.wallet.toLowerCase() ? (
            <Box
              style={{ marginLeft: "10px" }}
              onClick={() => deleteTask(v.id)}
            >

            </Box>
          ) : null}
        </Box>
      </Flex>
    ))(tasks)
 
  const NewTask = () => {
    const [newTask, setNewTask] = useState("")
 
    const handleAddBtnClick = async () => {
      if (!/^\s*$/.test(newTask)) {
        await addTask(newTask)
        setNewTask("")
      }
    }
 
    return (
      <Flex mb={4}>
        <Input
          placeholder="Enter New Task"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          sx={{ borderRadius: "5px 0 0 5px" }}
        />
        <Flex
          bg="#111"
          color="white"
          py={2}
          px={6}
          sx={{
            borderRadius: "0 5px 5px 0",
            cursor: "pointer",
            ":hover": { opacity: 0.75 },
          }}
          onClick={handleAddBtnClick}
        >
          add
        </Flex>
      </Flex>
    )
  }
 
  const Transactions = () => {
    return (
      <Flex justify="center" p={4}>
        <Box
          as="a"
          target="_blank"
          href={`https://sonar.warp.cc/?#/app/contract/${contractTxId}`}
          sx={{ textDecoration: "underline" }}
        >
          view transactions
        </Box>
      </Flex>
    )
  }
 
  return (
    <ChakraProvider>
      <NavBar />
      <Flex mt="60px" justify="center" p={3}>
        <Box w="100%" maxW="600px">
          <Tabs />
          {!isNil(user) ? <NewTask /> : null}
          <Tasks />
        </Box>
      </Flex>
      <Transactions />
    </ChakraProvider>
  )
}

Congrats! You have built a fully-decentralized Todo Manager Dapp from scratch using WeaveDB.

Go to localhost:3000 (opens in a new tab) and see how it works.

You can also access the entire dapp code at our Github repo (opens in a new tab).

Code walkthrough

Import Dependencies

import { useState, useEffect } from "react"
import lf from "localforage"
import { isNil, map } from "ramda"
import SDK from "weavedb-sdk"
import { ethers } from "ethers"
import { Box, Flex, Input, ChakraProvider } from "@chakra-ui/react"

Define Variables

💡

Change the string value "WEAVEDB_CONTRACT_TX_ID" to the contractTxId you was given when you Deploy a Database

const contractTxId = WEAVEDB_CONTRACT_TX_ID

Define React States

export default function App() {
  const [user, setUser] = useState(null)
  const [tasks, setTasks] = useState([])
  const [tab, setTab] = useState("All")
  const [initDB, setInitDB] = useState(false)
  const tabs = isNil(user) ? ["All"] : ["All", "Yours"]
  const [db, setDb] = useState(null)
  return (...)
}
  • user - logged in user
  • tasks - tasks to do
  • tab - current page tab
  • initDB - to determine if the WeaveDB is ready to use
  • tabs - page tab options, All to display everyone's tasks, Yours for only your tasks
  • db - the WeaveDB instance

Define Functions

setupWeaveDB

  const setupWeaveDB = async () => {
    const _db = new SDK({
      contractTxId,
    })
    await _db.init()
    setDb(_db)
    setInitDB(true)
  }

getTasks

  const getTasks = async () => {
    setTasks(await db.cget("tasks", ["date", "desc"]))
  }

getMyTasks

  const getMyTasks = async () => {
    setTasks(
      await db.cget(
        "tasks",
        ["user_address", "==", user.wallet.toLowerCase()],
        ["date", "desc"]
      )
    )
  }

addTask

  const addTask = async (task) => {
    await db.add(
      {
        task,
        date: db.ts(),
        user_address: db.signer(),
        done: false,
      },
      "tasks",
      user
    )
    await getTasks()
  }

completeTask

  const completeTask = async (id) => {
    await db.update(
      {
        done: true,
      },
      "tasks",
      id,
      user
    )
    await getTasks()
  }

deleteTask

  const deleteTask = async (id) => {
    await db.delete("tasks", id, user)
    await getTasks()
  }

login

We will generate a disposal account the first time a user logs in, link it with the Metamask address within WeaveDB, and save it locally in the browser's IndexedDB.

{ wallet, privateKey } is how we need to pass the user object to the SDK when making transactions, so we will save it like so.

  const login = async () => {
    const provider = new ethers.providers.Web3Provider(window.ethereum, "any")
    await provider.send("eth_requestAccounts", [])
    const wallet_address = await provider.getSigner().getAddress()
    let identity = await lf.getItem(
      `temp_address:${contractTxId}:${wallet_address}`
    )
    let tx
    let err
    if (isNil(identity)) {
      ;({ tx, identity, err } = await db.createTempAddress(wallet_address))
      const linked = await db.getAddressLink(identity.address)
      if (isNil(linked)) {
        alert("something went wrong")
        return
      }
    } else {
      await lf.setItem("temp_address:current", wallet_address)
      setUser({
        wallet: wallet_address,
        privateKey: identity.privateKey,
      })
      return
    }
    if (!isNil(tx) && isNil(tx.err)) {
      identity.tx = tx
      identity.linked_address = wallet_address
      await lf.setItem("temp_address:current", wallet_address)
      await lf.setItem(
        `temp_address:${contractTxId}:${wallet_address}`,
        JSON.parse(JSON.stringify(identity))
      )
      setUser({
        wallet: wallet_address,
        privateKey: identity.privateKey,
      })
    }
  }

logout

We will simply remove the current logged in state. The disposal address will be reused the next time the user logs in.

  const logout = async () => {
    if (confirm("Would you like to sign out?")) {
      await lf.removeItem("temp_address:current")
      setUser(null, "temp_current")
    }
  }

checkUser

When the page is loaded, check if the user is logged in.

  const checkUser = async () => {
    const wallet_address = await lf.getItem(`temp_address:current`)
    if (!isNil(wallet_address)) {
      const identity = await lf.getItem(
        `temp_address:${contractTxId}:${wallet_address}`
      )
      if (!isNil(identity))
        setUser({
          wallet: wallet_address,
          privateKey: identity.privateKey,
        })
    }
  }

Define Reactive State Changes

  useEffect(() => {
    checkUser()
    setupWeaveDB()
  }, [])
 
  useEffect(() => {
    if (initDB) {
      if (tab === "All") {
        getTasks()
      } else {
        getMyTasks()
      }
    }
  }, [tab, initDB])
  • When the page is loaded, check if the user is logged in and set up WeaveDB.
  • Get specified tasks, when the page tab is switched.

Define React Components

NavBar

  const NavBar = () => (
    <Flex p={3} position="fixed" w="100%" sx={{ top: 0, left: 0 }}>
      <Box flex={1} />
      <Flex
        bg="#111"
        color="white"
        py={2}
        px={6}
        sx={{
          borderRadius: "5px",
          cursor: "pointer",
          ":hover": { opacity: 0.75 },
        }}
      >
        {!isNil(user) ? (
          <Box onClick={() => logout()}>{user.wallet.slice(0, 7)}</Box>
        ) : (
          <Box onClick={() => login()}>Connect Wallet</Box>
        )}
      </Flex>
    </Flex>
  )

Tabs

  const Tabs = () => (
    <Flex justify="center" style={{ display: "flex" }} mb={4}>
      {map((v) => (
        <Box
          key={v}
          mx={2}
          onClick={() => setTab(v)}
          color={tab === v ? "red" : ""}
          textDecoration={tab === v ? "underline" : ""}
          sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
        >
          {v}
        </Box>
      ))(tabs)}
    </Flex>
  )

Tasks

  const Tasks = () =>
    map((v) => (
      <Flex
        key={v.id}
        sx={{ border: "1px solid #ddd", borderRadius: "5px" }}
        p={3}
        my={1}
      >
        <Box
          w="30px"
          textAlign="center"
          sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
        >
          {v.data.done ? (
            "✅"
          ) : v.data.user_address !== user?.wallet.toLowerCase() ? null : (
            <Box onClick={() => completeTask(v.id)}>⬜</Box>
          )}
        </Box>
        <Box px={3} flex={1} style={{ marginLeft: "10px" }}>
          {v.data.task}
        </Box>
        <Box w="100px" textAlign="center" style={{ marginLeft: "10px" }}>
          {v.data.user_address.slice(0, 7)}
        </Box>
        <Box
          w="50px"
          textAlign="center"
          sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
        >
          {v.data.user_address === user?.wallet.toLowerCase() ? (
            <Box
              style={{ marginLeft: "10px" }}
              onClick={() => deleteTask(v.id)}
            >

            </Box>
          ) : null}
        </Box>
      </Flex>
    ))(tasks)

NewTask

  const NewTask = () => {
    const [newTask, setNewTask] = useState("")
 
    const handleAddBtnClick = async () => {
      if (!/^\s*$/.test(newTask)) {
        await addTask(newTask)
        setNewTask("")
      }
    }
 
    return (
      <Flex mb={4}>
        <Input
          placeholder="Enter New Task"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          sx={{ borderRadius: "5px 0 0 5px" }}
        />
        <Flex
          bg="#111"
          color="white"
          py={2}
          px={6}
          sx={{
            borderRadius: "0 5px 5px 0",
            cursor: "pointer",
            ":hover": { opacity: 0.75 },
          }}
          onClick={handleAddBtnClick}
        >
          add
        </Flex>
      </Flex>
    )
  }

Transactions

  const Transactions = () => {
    return (
      <Flex justify="center" p={4}>
        <Box
          as="a"
          target="_blank"
          href={`https://sonar.warp.cc/?#/app/contract/${contractTxId}`}
          sx={{ textDecoration: "underline" }}
        >
          view transactions
        </Box>
      </Flex>
    )
  }

Return Components

  return (
    <ChakraProvider>
      <NavBar />
      <Flex mt="60px" justify="center" p={3}>
        <Box w="100%" maxW="600px">
          <Tabs />
          {!isNil(user) ? <NewTask /> : null}
          <Tasks />
        </Box>
      </Flex>
      <Transactions />
    </ChakraProvider>
  )