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.
The main goal of this project is to create a real dApp that uses Cedalio as a storage platform. We chose Polygon-Mumbai as our network. If you want you can choose another chain that we support.
If you want to jump in the code, please be our guest. Or you can use our live demo.
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.
To speed up the tutorial, we recommend cloning the repository and following it alongside the source code.
We use WalletConnect to login into our todo-app and filter the ToDos created by your wallet. To get a projectId you should sign up here and create a project. Once you have the projectId, set up a .env file for the Cedalio To-Do with an environment variable called REACT_APP_WC_PROJECT_ID. Learn more about wallet connect.
In every component created in this tutorial we have 3 main sections, the GraphQL query definitions with the gql notation, states to handle the changes and user inputs, and the JSX components used to render.
Note that we use the useAccount hook. This allows us to know if the wallet is already connected or not, and send it to the ListComponent, which validates if the address exists (user is connected) to render the list of ToDos.
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.
We have React + Apollo as our frontend-client and Cedalio as our GraphQL-backend, using Polygon Mumbai as a network.
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
The schema look like this:
type Todo {
id: UUID!
title: String!
description: String
priority: Int!
owner: String!
tags: [String!]
status: String
}
Then we can use Cedalio CLI to compile the Schema to verify that it’s correct. If you don’t have our CLI already installed, follow these steps.
>bifrost compile --schema-file /path/to/your/todo.graphql
Something useful is exporting the Schema in order to see all the mutations and queries that Cedalio automatically creates for your schema. You can also review the input types for all the mutations. To export the schema run:
bifrost export-schema --schema-file /path/to/your/todo.graphql
This is the output you get. If you prefer you can > it to a file :D:
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.
Now you are ready to deploy and serve your schema, but first we have to set an environment variable — our Metamask private key address — . Be very cautious when using this key. Keep in mind that we only access your private key to sign the transactions that are sent to the blockchain of your choice when deploying a schema or executing a mutation. Here you can find more information about this.
export PRIVATE_KEY='your-private-key'
Please, verify that you have enough tokens to deploy this smart contract and to run future transactions. If you need more tokens you have two options: Polygon Faucet or Alchemy Faucet.
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
Once your schema is deployed, you are ready to start a local GraphQL server and execute queries and mutations. This local GraphQL server is intended to be used for development and testing purposes. To start the server all you need to do is run the following:
bifrost serve --schema-name todo-v1
As shown in the output log, you can copy the address of the database smart-contract in the blockchain’s explorer. This is helpful to understand how transactions are executed against the smart-contract database.
Once a GraphQL server is running you can execute queries and mutations. By default the GraphQL endpoint is available at http://localhost:8080/graphql
. Whith the server running, now we can create a ToDo
object in the ToDo schema by executing the createTodo
mutation which is automatically generated by Bifrost.
if you want to test your server you can create ToDo item running:
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'
And get all the ToDos running:
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
}
}
`;
Then we create the variables and types that we need (title, description, priority, owner, tag and status) and then we add them to the createTodo mutation. You can use the exported schema to verify the needed mutation input types.
We are using the react states to handle the user inputs and errors:
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]
We are using the useMutation hook of apollo-client to link the execution of our mutation:
const [createTodo, { data, loading, error }] = useMutation(CREATE_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
}
Then we need to define our query in the ListComponent.tsx:
const GET_TODOS = gql`
query GetTodos{
allTodos {
id
title
description
priority
owner
tags
status
}
}
`;
To render the list of created ToDos in our account we use the useQuery hook of apollo-client.
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>;
Create a component that displays the ToDos as cards:
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} />
))
We define the status “ready” as the default. The other valid states are “done” and “delete”.
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
}
}
`;
This receives 2 variables, the UUID of the task we want to change and the field (in this case the status) that we need to update. As we have done previously, we need the updateTodo function from the useMutation hook.
const [updateTodo, { data, loading, error }] = useMutation(UPDATE_TODO);
We also want to run the mutation for updating when the user drags the ToDo in the delete/done zones, this requires the beautiful dnd library that allows us to define our todo-card as Draggable, and the 3 columns that we need (delete, ready and done) as Droppable. The card of the To Dos must have the tag <Draggable />
<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"
}
And include it in the displayTodos function. Notice that we filter the query results and consider “displayable” the ones that have 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}/>
))
)
}
}
When the transaction in the blockchain is done and confirmed, we see a toast notification at the bottom left as a notification to the user.
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.
Finally you can also chose a different network or chain to deploy your DB by just changing the deployment flag and that is all you need to do.