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 setsigner
date
must be theblock.timestamp
done
must default tofalse
- Only
done
can be updated totrue
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.
- WeaveDB SDK (opens in a new tab) - to connect with WeaveDB
- Ramda.js (opens in a new tab) - functional programming utilities
- Chakra UI (opens in a new tab) - UI library
- Ethers.js (opens in a new tab) - to connect with Metamask
- localForage (opens in a new tab) - IndexedDB wrapper to store a disposal wallet
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
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 usertasks
- tasks to dotab
- current page tabinitDB
- to determine if the WeaveDB is ready to usetabs
- page tab options,All
to display everyone's tasks,Yours
for only your tasksdb
- 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>
)