How to create a To-Do dApp using Vercel + Cedalio

Introduction

This tutorial assumes a basic knowledge of React, Typescript and blockchain concepts such as wallet and private key.

Requirements

  • Metamask Wallet (At the moment we support Ethereum, Polygon, Metis and Hard Hat) For this Example we will use Polygon Mumbai. Please add the polygon-mumbai network to your wallet.
  • NodeJS 14v
  • NPM or YARN
  • NPX
  • React Developer Tools
  • Material UI (style component)
  • Web3modal (Wallet Connect)
  • Beautiful dnd react
  • Apollo Client Devtool
  • Cedalio CLI

Get Started

In this tutorial we want to showcase how to create a To-Do app that uses Cedalio as a storage layer. We focus on storing data on-chain and consuming it with a client using a simple GraphQL schema and a client like ApolloClient in a reactApp. We get the data from the blockchain using the Apollo GraphQL client implemented in TypeScript. We decide to create one Database Smart Contract for all the users and filter each client’s ToDos on the frontend.

One Smart Contract Database for all the users.
 const { address } = useAccount()

Define the dApp Architecture

We have two different architecture approaches using Cedalio. You can have one Smart Contract Database for each user, or a Smart Contract Database for all the users. In the first option all the users are the owners of the Smart Contract holding the information, and in the other option the dApp admin is the Owner of the Smart Contract Database. For this example we chose the second option (one database for all the users) to simplify the example and to focus on creating an end-to-end dApp using our tech. In the next post we will talk about how to implement one Database for each user.

Define the GraphQL Schema

First, we need to define our base GraphQL schema, we create an object type to store the to-dos and a file called todo.graphql

type Todo {
id: UUID!
title: String!
description: String
priority: Int!
owner: String!
tags: [String!]
status: String
}
>bifrost compile --schema-file /path/to/your/todo.graphql
bifrost export-schema --schema-file /path/to/your/todo.graphql
scalar UUID

enum RelationType {
OWNS
REFERENCES
EMBEDDED
BELONGS_TO
}

directive @belongsTo(type: String!) on OBJECT

directive @relation(type: String!, via: String) on FIELD_DEFINITION

directive @embedded on FIELD_DEFINITION

directive @belongsTo(via: String!) on FIELD_DEFINITION

type Todo {
id: UUID!
title: String!
description: String
priority: Int!
owner: String!
tags: [String!]
status: String
}

input TodoInput {
title: String!
description: String
priority: Int!
owner: String!
tags: [String!]
status: String
}

input TodoUpdateInput {
title: String
description: String
priority: Int
owner: String
tags: [String!]
status: String
}

type Query {
todoById(id: UUID!): Todo
allTodos: [Todo!]!
}

type Mutation {
createTodo(todo: TodoInput!): Todo!
updateTodo(id: UUID!, fields: TodoUpdateInput!): Todo!
}

schema {
query: Query
mutation: Mutation
}

Deploying your Schema

Once you have defined your schema you can deploy it and start executing mutations and queries against it. Currently, to deploy your schema you require a wallet with enough tokens of the blockchain where you want to deploy your schema. There are many options to choose from. We recommend MetaMask.

export PRIVATE_KEY='your-private-key'
bifrost deploy --schema-file /path/to/your/todo.graphql --network polygon-mumbai
Please enter the schema name: todo-v1
✅ Schema '/Users/nicomagni/Desktop/Cedalio/ToDo/todo.graphql' was successfully compiled!


Deploying Graphql schema 'todo.graphql' with wallet address '0x#########################################' in network 'polygon-mumbai'... 🚀

📣 deploying may take between 10 to 30 seconds depending on network congestion

✅ Schema 'todo-v1' was successfully deployed!

- The configuration file was saved at: '/Users/USER/.bifrost/todo-v1/deployment.json'
- The smart-contract database address is: '0xd8771c6e0c3f01fd78b5e9c301893c4633180fc9'
- The address of the library contract is: '0xb3eb32b495ecf76c3788e7a6acafdca0952d4654'
- You can see the database contract here: 'https://mumbai.polygonscan.com/address/0xd8771c6e0c3f01fd78b5e9c301893c4633180fc9'

You can launch GraphQL server for this schema by executing
bifrost serve --schema-name todo-v1
bifrost serve --schema-name todo-v1
curl -v -H 'Content-Type: application/json' \
-d '{"query":"mutation { createTodo(todo: { title:\"Something\", priority: 1, owner:\"0x305d4bcaE378094F1923f8BB352824D9496510b1\",tags:[\"health\",\"rutine\"]\n}) { id title tags} }"}' \
'http://localhost:8080/graphql'
curl -v -H 'Content-Type: application/json' \
-d '{"query":"query { allTodos { id title tags status } }"}' \
'http://localhost:8080/graphql'

Creating React App

Now we are going to develop the React app. If you prefer you can start an app from scratch or clone our repo and follow the instructions below in order to understand how it works.

Add a Todo

In our TodoInputComponent we have to use the apollo-client to make the mutation that creates a new Todo. First we define our mutation in our TodoInputComponent.tsx:

const CREATE_TODO = gql`
mutation CreateTodo($title:String!, $description:String, $priority:Int!, $owner:String!,$tags:String, $status:String){
createTodo(todo: {title: $title, description:$description, priority:$priority, owner:$owner, tags:$tags, status: $status }){
id
title
description
priority
owner
tags
status
}
}
`;
const [priority, setPriority] = React.useState("");
const [title, setTitle] = React.useState("");
const [description, setDescription] = React.useState("");
const [titleError, setTitleError] = React.useState(false);
const [descriptionError, setDescriptionError] = React.useState(false);
const [tag, setTag] = React.useState<string[]>([]);
const priorities = [1, 2, 3, 4]
const [createTodo, { data, loading, error }] = useMutation(CREATE_TODO);
UI Component to create a ToDo.

Render the ToDo

First we need to define our ‘todo’ type to handle the todo’s across the application.

type Todo = {
title: string,
description?: string,
tags: Array<string>,
priority: number,
id: string,
owner: string,
status: string
}
const GET_TODOS = gql`
query GetTodos{
allTodos {
id
title
description
priority
owner
tags
status
}
}
`;
const { loading, error, data } = useQuery(GET_TODOS);


React.useEffect(() => {
if (data) setTodos(data.allTodos);
}, [data]);


if (loading) return <p>Loading...</p>;
if (error) {
return <p>Error : {error.message}</p>;
todos.filter((todo) => todo.status === "ready").map((todo: Todo, index) => (
<CardComponent key={todo.id}
todo={todo}
ownerAddress={ownerAddress}
setState={setTodos}
index={index}
updateState={update}
onUpdateTodo={onUpdateTodo} />
))

Mark as Done and Delete

We use the update mutation to change the status of a ToDo, we are using the same design pattern that we used in the past. The CardComponent.tsx looks like this:

const UPDATE_TODO = gql`
mutation UpdateTodo($id:UUID!, $status:String){
updateTodo(id: $id, fields:{status: $status} ){
id
title
description
priority
owner
tags
status
}
}
`;
const [updateTodo, { data, loading, error }] = useMutation(UPDATE_TODO);
<DragDropContext onDragEnd={onDragEnd} onDragStart={onDragStart} onDragUpdate={onDragUpdate}>
<div className="container">
<Droppable droppableId='delete'>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} className="delete-container">
<HighlightOffIcon fontSize="large" sx={{ height: "200px", width: "200px", color: deleteIconColor }} />
{provided.placeholder}
</div>
)}
</Droppable>
<Droppable droppableId='ready'>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} className="list-container">
{displayTodos()}
{provided.placeholder}
</div>
)}
</Droppable>


<Droppable droppableId='done'>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} className="done-container">
<CheckCircleOutlineIcon fontSize="large" sx={{ height: "200px", width: "200px", color: doneIconColor }} />
{provided.placeholder}
</div>
)}
</Droppable>
</div>
<TodoInputComponent setState={setNewTodo} address={ownerAddress} />
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
The operation was successfull
</Alert>
</Snackbar>
</DragDropContext>

Add an Empty State

We need to have an empty state ToDo item for our users when they first arrive and don’t have any ToDo items created yet.

const defaultTodo = {
title: "This Is Your First ToDo Card!",
description: "Buy some food for my dog and change their water",
tags: ["healt", "rutine"],
priority: 1,
id: "abcdefg12345",
owner: "abcdefg123",
status: "ready"
}
const displayTodos = () => {
const displayableTodos = todos.filter((todo) => todo.status === "ready")
if (displayableTodos.length === 0) {
return <CardComponent key="default" todo={defaultTodo} ownerAddress={defaultTodo.owner} setState={setTodos} index={1} updateState={update} onUpdateTodo={onUpdateTodo} default={true}/>;
}
else {
return (
displayableTodos.map((todo: Todo, index) => (
<CardComponent key={todo.id} todo={todo} ownerAddress={ownerAddress} setState={setTodos} index={index} updateState={update} onUpdateTodo={onUpdateTodo} default={false}/>
))
)
}
}

Conclusion

With Cedalio we can create a dApp just by defining the data model and Cedalio automatically creates the resolvers for you. You can also store all the information on-chain without struggling with solidity or smart contracts.

--

--

Just define your data model and the business logic you want to decentralize, without having to worry about storage management or smart contract coding.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Cedalio

Just define your data model and the business logic you want to decentralize, without having to worry about storage management or smart contract coding.