Artigo original: How to Build a TodoApp using ReactJS and Firebase
Olá, pessoal. Boas-vindas a este tutorial. Antes de começarmos, você deve estar familiarizado com os conceitos básicos do ReactJS. Se não está, eu recomendo que você veja a documentação do ReactJS (em inglês).
Usaremos os seguintes componentes na aplicação:
Como nossa aplicação ficará:


Arquitetura da aplicação:

Entendendo nossos componentes:
Você talvez esteja se perguntando o motivo de estarmos usando o Firebase nessa aplicação. Bem, ele fornece autenticação segura, um banco de dados em tempo real, um componente serverless (em português, sem servidor) e um bucket de armazenamento.
Estamos usando o Express aqui. Assim, não precisaremos lidar com as exceções do HTTP. Usaremos todos os pacotes do Firebase em nossos componentes funcionais. Não queremos deixar o client da aplicação muito grande, o que tende a deixar o processo de carregamento da interface lento.
Observação: dividirei este tutorial em quatro seções separadas. Você encontrará um git commit com o código desenvolvido para cada seção. Se você quiser ver o código completo, também, ele estará disponível neste repositório.
Seção 1: desenvolvimento das APIs de afazeres
Nesta seção, vamos desenvolver os seguintes elementos:
- Configurar as funções do Firebase.
- Instalar o framework do Express e criar as APIs.
- Configurar o firestore como banco de dados.
O código da API implementada nesta seção pode ser encontrado neste commit.
Configurar as funções do firebase:
Vá até o Firebase console.

Selecione a opção Add Project (Adicionar projeto). Depois disso, siga o passo a passo do gif abaixo para configurar o projeto do Firebase.

Vá até a aba Functions (Funções) e clique no botão Get Started (Iniciar):

Você verá uma caixa de diálogo com instruções sobre Como configurar as funções do firebase. Vá até seu ambiente local. Abra a ferramenta de linha de comando. Para instalar as ferramentas do Firebase em sua máquina, use o comando abaixo:
npm install -g firebase-tools
Quando estiver concluído, use o comando firebase init
para configurar as funções do Firebase em seu ambiente local. Selecione as seguintes opções quando estiver inicializando uma função do Firebase no ambiente local:
- Quais funcionalidades do Firebase CLI você quer configurar para esta pasta? Pressione a barra de espaço para selecionar as funcionalidades. Depois, aperte Enter para confirmar suas escolhas => Functions: Configure and deploy Cloud Functions
- Primeiro, vamos associar o diretório deste projeto com o projeto do Firebase… => Use an existing project
- Selecione um projeto padrão do Firebase para este diretório => application_name
- Com qual linguagem você gostaria de escrever as Cloud Functions? => JavaScript
- Você quer usar ESLint para saber sobre prováveis bugs e estilizar? => N
- Você quer instalar dependências com o npm agora? (Y/n) => Y
Depois que a configuração estiver concluída, você verá a seguinte mensagem:
✔ Firebase initialization complete!
Quando a inicialização estiver completa, a estrutura do nosso diretório ficará assim:
+-- firebase.json
+-- functions
| +-- index.js
| +-- node_modules
| +-- package-lock.json
| +-- package.json
Agora, abra o index.js
no diretório functions
e copie e cole o seguinte código:
const functions = require('firebase-functions');
exports.helloWorld = functions.https.onRequest((request, response) => {
response.send("Hello from Firebase!");
});
Envie o código para as funções do Firebase usando o seguinte comando:
firebase deploy
Assim que o envio concluir, você verá a seguinte linha de registro no fim da sua linha de comando:
> ✔ Deploy complete!
> Project Console: https://console.firebase.google.com/project/todoapp-<id>/overview
Vá até Project Console > Functions. Lá, você encontrará o URL da API. O URL será assim:
https://<região-de-hosting>-todoapp-<id>.cloudfunctions.net/helloWorld
Copie esse URL e cole-o no navegador. Você receberá a seguinte resposta:
Hello from Firebase!
Isso confirma que nossa função do Firebase foi configurada corretamente.
Instalar o framework do Express:
Agora vamos instalar o framework do Express
em nosso projeto usando o seguinte comando:
npm i express
Vamos criar um diretório APIs dentro do diretório functions. Dentro desse diretório, vamos criar um arquivo chamado todos.js
. Remova tudo do arquivo index.js
e copie e cole o seguinte código:
//index.js
const functions = require('firebase-functions');
const app = require('express')();
const {
getAllTodos
} = require('./APIs/todos')
app.get('/todos', getAllTodos);
exports.api = functions.https.onRequest(app);
Atribuímos a função getAllTodos à rota /todos. Então, todas as chamadas à API nesta rota executarão a partir da função getAllTodos. Em seguida, vá até o arquivo todos.js
no diretório APIs e vamos escrever a função getAllTodos.
//todos.js
exports.getAllTodos = (request, response) => {
todos = [
{
'id': '1',
'title': 'greeting',
'body': 'Hello world from sharvin shah'
},
{
'id': '2',
'title': 'greeting2',
'body': 'Hello2 world2 from sharvin shah'
}
]
return response.json(todos);
}
Aqui, declaramos um objeto JSON de exemplo. Depois, vamos extrai-lo a partir do Firestore. Por agora, no entanto, vamos retornar isso. Envie isso para sua função do Firebase usando o comando firebase deploy
. Ele pedirá permissão para apagar o módulo helloworld – apenas aperte Enter
e y
.
The following functions are found in your project but do not exist in your local source code: helloWorld
Would you like to proceed with deletion? Selecting no will continue the rest of the deployments. (y/N) y
Depois de concluído, vá até Project Console > Functions. Lá, você encontrará o URL da API, que será algo como:
https://<região-de-hosting>-todoapp-<id>.cloudfunctions.net/api
Agora, vá ao navegador e copie e cole o URL, adicionando /todos no fim do URL. Você receberá o seguinte resultado:
[
{
'id': '1',
'title': 'greeting',
'body': 'Hello world from sharvin shah'
},
{
'id': '2',
'title': 'greeting2',
'body': 'Hello2 world2 from sharvin shah'
}
]
Firebase Firestore:
Usaremos o firestore do Firebase como banco de dados em tempo real para nossa aplicação. Vá até Console > Database no Firebase Console. Para configurar o firestore, siga o gif abaixo:

Quando a configuração terminar, clique no botão Start Collection e indique Collection ID como sendo todos. Clique em Next e receberá o seguinte pop-up:

Ignore a chave DocumentID. Para preencher field, type e value, veja o JSON abaixo. Atualize o valor de acordo:
{
Field: title,
Type: String,
Value: Hello World
},
{
Field: body,
Type: String,
Value: Hello folks I hope you are staying home...
},
{
Field: createtAt,
type: timestamp,
value: Add the current date and time here
}
Pressione o botão de salvar. Você verá que a coleção e o documento foram criados. Volte para o ambiente local. Precisamos instalar firebase-admin
, que possui o pacote firestore de que precisamos. Use este comando para instalá-lo:
npm i firebase-admin
Crie um diretório chamado util no diretório functions. Vá até esse diretório e crie um arquivo chamado admin.js
. Neste arquivo, vamos importar o pacote firebase admin e inicializar o objeto do banco de dados do firestore. Vamos exportá-lo para que outros módulos possam usá-lo.
//admin.js
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
module.exports = { admin, db };
Agora, vamos escrever uma API para buscar esses dados. Vá até todos.js
, no diretório functions > APIs. Remova o código antigo e copie e cole o código abaixo:
//todos.js
const { db } = require('../util/admin');
exports.getAllTodos = (request, response) => {
db
.collection('todos')
.orderBy('createdAt', 'desc')
.get()
.then((data) => {
let todos = [];
data.forEach((doc) => {
todos.push({
todoId: doc.id,
title: doc.data().title,
body: doc.data().body,
createdAt: doc.data().createdAt,
});
});
return response.json(todos);
})
.catch((err) => {
console.error(err);
return response.status(500).json({ error: err.code});
});
};
Aqui estamos buscando todos os afazeres do banco de dados e enviando-os para o client como uma lista.
Você também pode executar a aplicação localmente usando o comando firebase serve
ao invés de enviar toda vez. Quando você executar esse comando, talvez apareça um erro relacionado às credenciais. Para consertar, siga os passos mencionados abaixo:
- Vá até Project Settings (ícone de engrenagem ao lado esquerdo no topo)
- Vá até a aba Service Accounts
- Lá embaixo, você verá a opção de Generating a new key. Clique nessa opção e você baixará um arquivo JSON.
- Precisamos exportar essas credenciais para nossa seção de linha de comando. Use o comando abaixo para fazer isso:
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/[NOME_DO_ARQUIVO].json"
Depois disso, execute o comando firebase serve. Se você ainda tiver um erro, use o seguinte comando: firebase login --reauth
. Ele abrirá a página para fazer registro com o Google no navegador. Depois de completar o registro, ele funcionará sem erros.
Você encontrará um URL nos registros da sua ferramenta de linha de comando quando executar o comando firebase serve
. Abra esse URL no navegador e digite /todos
no fim.
✔ functions[api]: http function initialized (http://localhost:5000/todoapp-<project-id>/<nome-da-região>/api).
Você terá o seguinte resultado em JSON no seu navegador:
[
{
"todoId":"W67t1kSMO0lqvjCIGiuI",
"title":"Hello World",
"body":"Hello folks I hope you are staying home...",
"createdAt":{"_seconds":1585420200,"_nanoseconds":0 }
}
]
Escrevendo outras APIs:
Está na hora de escrever outras APIs das quais precisaremos na nossa aplicação.
- Crie um afazer: vá até
index.js
no diretório functions. Importe o método postOneTodo abaixo de getAllTodos. Atribua, também, a rota POST para esse método.
//index.js
const {
..,
postOneTodo
} = require('./APIs/todos')
app.post('/todo', postOneTodo);
Vá até todos.js
dentro do diretório de funções e adicione um novo método postOneTodo
abaixo do método getAllTodos
.
//todos.js
exports.postOneTodo = (request, response) => {
if (request.body.body.trim() === '') {
return response.status(400).json({ body: 'Must not be empty' });
}
if(request.body.title.trim() === '') {
return response.status(400).json({ title: 'Must not be empty' });
}
const newTodoItem = {
title: request.body.title,
body: request.body.body,
createdAt: new Date().toISOString()
}
db
.collection('todos')
.add(newTodoItem)
.then((doc)=>{
const responseTodoItem = newTodoItem;
responseTodoItem.id = doc.id;
return response.json(responseTodoItem);
})
.catch((err) => {
response.status(500).json({ error: 'Something went wrong' });
console.error(err);
});
};
Nesse método, estamos adicionando um novo afazer em nosso banco de dados. Se os elementos do nosso conteúdo estiverem vazios, retornaremos a resposta 400. Caso contrário, vamos adicionar os dados.
Execute o comando firebase serve
e abra o Postman. Crie uma requisição e selecione o método de tipo POST. Adicione o URL e um body
do tipo JSON.
URL: http://localhost:5000/todoapp-<app-id>/<nome-da-região>/api/todo
METHOD: POST
Body: {
"title":"Hello World",
"body": "We are writing this awesome API"
}
Pressione o botão de enviar e você terá a seguinte resposta:
{
"title": "Hello World",
"body": "We are writing this awesome API",
"createdAt": "2020-03-29T12:30:48.809Z",
"id": "nh41IgARCj8LPWBYzjU0"
}
2. Apague um afazer: vá até index.js
no diretório functions. Importe o método deleteTodo abaixo do postOneTodo. Atribua a rota DELETE para esse método também.
//index.js
const {
..,
deleteTodo
} = require('./APIs/todos')
app.delete('/todo/:todoId', deleteTodo);
Vá até todos.js
e adicione um novo método deleteTodo
abaixo do método postOneTodo
.
//todos.js
exports.deleteTodo = (request, response) => {
const document = db.doc(`/todos/${request.params.todoId}`);
document
.get()
.then((doc) => {
if (!doc.exists) {
return response.status(404).json({ error: 'Todo not found' })
}
return document.delete();
})
.then(() => {
response.json({ message: 'Delete successfull' });
})
.catch((err) => {
console.error(err);
return response.status(500).json({ error: err.code });
});
};
Nesse método, estamos excluindo um afazer do nosso banco de dados. Execute o comando firebase serve
e vá até o Postman. Crie uma requisição, selecione o tipo DELETE para o método e adicione o URL.
URL: http://localhost:5000/todoapp-<app-id>/<nome-da-região>/api/todo/<todo-id>
METHOD: DELETE
Pressione o botão de enviar e você terá a seguinte resposta:
{
"message": "Delete successfull"
}
3. Edite um afazer: vá até index.js
no diretório functions. Importe o método editTodo abaixo de deleteTodo. Atribua a rota PUT a esse método.
//index.js
const {
..,
editTodo
} = require('./APIs/todos')
app.put('/todo/:todoId', editTodo);
Vá até todos.js
e adicione um novo método editTodo
abaixo do método deleteTodo
já existente.
//todos.js
exports.editTodo = ( request, response ) => {
if(request.body.todoId || request.body.createdAt){
response.status(403).json({message: 'Not allowed to edit'});
}
let document = db.collection('todos').doc(`${request.params.todoId}`);
document.update(request.body)
.then(()=> {
response.json({message: 'Updated successfully'});
})
.catch((err) => {
console.error(err);
return response.status(500).json({
error: err.code
});
});
};
Nesse método, estamos editando um afazer da nossa base de dados. Lembre-se de que não estamos permitindo que o usuário edite os campos todoId ou createdAt. Execute o comando firebase serve
e vá até o Postman. Crie uma requisição, selecione o tipo PUT para o método e adicione o URL.
URL: http://localhost:5000/todoapp-<app-id>/<nome-da-região>/api/todo/<todo-id>
METHOD: PUT
Pressione o botão de enviar e você terá a seguinte resposta:
{
"message": "Updated successfully"
}
Estrutura do diretório até agora:
+-- firebase.json
+-- functions
| +-- API
| +-- +-- todos.js
| +-- util
| +-- +-- admin.js
| +-- index.js
| +-- node_modules
| +-- package-lock.json
| +-- package.json
| +-- .gitignore
Com isso, completamos a primeira seção da aplicação. Você pode ir tomar um café, descansar e, depois, vamos trabalhar no desenvolvimento das APIs dos usuários.
Seção 2: desenvolvimento das APIs dos usuários
Nesta seção, vamos desenvolver estes componentes:
- API de autenticação do usuário (login e registro).
- API para obter e atualizar os detalhes do usuário.
- API para atualizar a foto de perfil do usuário.
- API de afazeres mais segura.
O código da API de usuários implementado nesta seção pode ser encontrado neste commit.
Então, vamos começar construindo a API de autenticação do usuário. Vá até Firebase console > Authentication.

Clique no botão Set up sign-in method. Vamos usar e-mail e senha para a validação do usuário. Habilite a opção Email/Password.

Agora, vamos criar nosso usuário manualmente. Primeiro, vamos criar a API de login. Depois, vamos criar a API de registro.
Vá até a aba Users, em Authentication, preencha os detalhes do usuário e clique no botão Add User.

1. API de login do usuário:
Primeiro, precisamos instalar o pacote firebase
, que consiste na biblioteca de autenticação do Firebase, usando o seguinte comando:
npm i firebase
Quando a instalação estiver concluída, vá até o diretório functions > APIs. Aqui, vamos criar um arquivo users.js
. Agora, dentro de index.js
, importamos um método loginUser e atribuímos a ele a rota POST.
//index.js
const {
loginUser
} = require('./APIs/users')
// Users
app.post('/login', loginUser);
Vá até Project Settings > General. Lá, você encontrará o seguinte card:

Selecione o ícone da Web e então siga o gif abaixo:

Selecione a opção Continue to console. Quando estiver concluído, você verá um JSON com as configurações do Firebase. Vá até o diretório functions > util e crie um arquivo config.js
. Copie e cole o seguinte código nesse arquivo:
// config.js
module.exports = {
apiKey: "............",
authDomain: "........",
databaseURL: "........",
projectId: ".......",
storageBucket: ".......",
messagingSenderId: "........",
appId: "..........",
measurementId: "......."
};
Substitua ............
pelos valores que estão em Firebase console > Project settings > General > Your apps > Firebase SD snippet > Config.
Copie e cole o seguinte código no arquivo users.js
:
// users.js
const { admin, db } = require('../util/admin');
const config = require('../util/config');
const firebase = require('firebase');
firebase.initializeApp(config);
const { validateLoginData, validateSignUpData } = require('../util/validators');
// Login
exports.loginUser = (request, response) => {
const user = {
email: request.body.email,
password: request.body.password
}
const { valid, errors } = validateLoginData(user);
if (!valid) return response.status(400).json(errors);
firebase
.auth()
.signInWithEmailAndPassword(user.email, user.password)
.then((data) => {
return data.user.getIdToken();
})
.then((token) => {
return response.json({ token });
})
.catch((error) => {
console.error(error);
return response.status(403).json({ general: 'wrong credentials, please try again'});
})
};
Estamos usando o módulo signInWithEmailAndPassword do Firebase para verificar se as credenciais enviadas pelo usuário estão certas. Se estiverem, então enviamos o token desse usuário. Caso contrário, enviamos um status 403 com mensagem "wrong credentials" (credenciais incorretas).
Agora vamos criar o arquivo validators.js
no diretório functions > util. Copie e cole o seguinte código nesse arquivo:
// validators.js
const isEmpty = (string) => {
if (string.trim() === '') return true;
else return false;
};
exports.validateLoginData = (data) => {
let errors = {};
if (isEmpty(data.email)) errors.email = 'Must not be empty';
if (isEmpty(data.password)) errors.password = 'Must not be empty';
return {
errors,
valid: Object.keys(errors).length === 0 ? true : false
};
};
Com isso, a API de login está completa. Execute o comando firebase serve
e vá até o Postman. Crie uma requisição, selecione o método POST, adicione o URL e o conteúdo.
URL: http://localhost:5000/todoapp-<app-id>/<nome-da-região>/api/login
METHOD: POST
Body: {
"email":"Add email that is assigned for user in console",
"password": "Add password that is assigned for user in console"
}
Aperte o botão para enviar a requisição no Postman e você receberá o seguinte resultado:
{
"token": ".........."
}
Usaremos esse token em uma parte futura para obter os detalhes do usuário. Lembre-se de que esse token expira em 60 minutos. Para gerar um novo token, use esta API novamente.
2. API de registro de usuário:
O mecanismo padrão de autenticação do Firebase permite apenas que você armazene informações como e-mail, senha, etc. Precisamos, porém, de mais informações para identificar se esse usuário possui esse afazer para que possa haver a leitura, a atualização e a exclusão.
Para isso, vamos criar uma coleção chamada users. Nessa coleção, vamos armazenar os dados do usuário que serão mapeados para o afazer baseado no nome de usuário. Cada nome de usuário será único para todos os usuários na plataforma.
Vá até index.js
. Importaremos o método signUpUser e atribuiremos a ele a rota POST.
//index.js
const {
..,
signUpUser
} = require('./APIs/users')
app.post('/signup', signUpUser);
Agora, vá até validators.js
e adicione o seguinte código abaixo do método validateLoginData
.
// validators.js
const isEmail = (email) => {
const emailRegEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (email.match(emailRegEx)) return true;
else return false;
};
exports.validateSignUpData = (data) => {
let errors = {};
if (isEmpty(data.email)) {
errors.email = 'Must not be empty';
} else if (!isEmail(data.email)) {
errors.email = 'Must be valid email address';
}
if (isEmpty(data.firstName)) errors.firstName = 'Must not be empty';
if (isEmpty(data.lastName)) errors.lastName = 'Must not be empty';
if (isEmpty(data.phoneNumber)) errors.phoneNumber = 'Must not be empty';
if (isEmpty(data.country)) errors.country = 'Must not be empty';
if (isEmpty(data.password)) errors.password = 'Must not be empty';
if (data.password !== data.confirmPassword) errors.confirmPassword = 'Passowrds must be the same';
if (isEmpty(data.username)) errors.username = 'Must not be empty';
return {
errors,
valid: Object.keys(errors).length === 0 ? true : false
};
};
Vá até users.js
e adicione o seguinte código abaixo do módulo loginUser
.
// users.js
exports.signUpUser = (request, response) => {
const newUser = {
firstName: request.body.firstName,
lastName: request.body.lastName,
email: request.body.email,
phoneNumber: request.body.phoneNumber,
country: request.body.country,
password: request.body.password,
confirmPassword: request.body.confirmPassword,
username: request.body.username
};
const { valid, errors } = validateSignUpData(newUser);
if (!valid) return response.status(400).json(errors);
let token, userId;
db
.doc(`/users/${newUser.username}`)
.get()
.then((doc) => {
if (doc.exists) {
return response.status(400).json({ username: 'this username is already taken' });
} else {
return firebase
.auth()
.createUserWithEmailAndPassword(
newUser.email,
newUser.password
);
}
})
.then((data) => {
userId = data.user.uid;
return data.user.getIdToken();
})
.then((idtoken) => {
token = idtoken;
const userCredentials = {
firstName: newUser.firstName,
lastName: newUser.lastName,
username: newUser.username,
phoneNumber: newUser.phoneNumber,
country: newUser.country,
email: newUser.email,
createdAt: new Date().toISOString(),
userId
};
return db
.doc(`/users/${newUser.username}`)
.set(userCredentials);
})
.then(()=>{
return response.status(201).json({ token });
})
.catch((err) => {
console.error(err);
if (err.code === 'auth/email-already-in-use') {
return response.status(400).json({ email: 'Email already in use' });
} else {
return response.status(500).json({ general: 'Something went wrong, please try again' });
}
});
}
Validamos os dados do nosso usuário. Então, enviamos um e-mail e senha para o módulo createUserWithEmailAndPassword do Firebase para criar o usuário. Depois que o usuário foi criado com sucesso, salvamos as credenciais do usuário no banco de dados.
Com isso, a API de registro está completa. Execute o comando firebase serve
e vá até o Postman. Crie uma requisição, selecione o método POST. Adicione o URL e o conteúdo.
URL: http://localhost:5000/todoapp-<app-id>/<region-name>/api/signup
METHOD: POST
Body: {
"firstName": "Add a firstName here",
"lastName": "Add a lastName here",
"email":"Add a email here",
"phoneNumber": "Add a phone number here",
"country": "Add a country here",
"password": "Add a password here",
"confirmPassword": "Add same password here",
"username": "Add unique username here"
}
Aperte o botão para enviar a requisição no Postman e você receberá o seguinte resultado:
{
"token": ".........."
}
Agora, vá até Firebase console > Database e, lá, você verá o seguinte resultado:

Como pode ver, nossa coleção de usuário foi criada com sucesso com um documento nela.
3. Carregar a foto de perfil do usuário:
Nossos usuários serão capazes de carregar suas fotos de perfil. Para isso, estaremos usando o bucket Storage. Vá até Firebase console > Storage e clique no botão Get started. Siga o GIF abaixo para saber como configurar:

Agora, vá na aba Rules, em Storage, e atualize a permissão para o acesso ao bucket, como na imagem abaixo:

Para carregar a foto de perfil, usaremos o pacote chamado busboy
. Para instalar o pacote, use o seguinte comando:
npm i busboy
Vá até index.js
. Importe o método uploadProfilePhoto abaixo do método signUpUser. Também atribua a rota POST para ele.
//index.js
const auth = require('./util/auth');
const {
..,
uploadProfilePhoto
} = require('./APIs/users')
app.post('/user/image', auth, uploadProfilePhoto);
Aqui, adicionamos uma camada de autenticação para que apenas um usuário associado àquela conta possa carregar a imagem. Agora, crie um arquivo chamado auth.js
no diretório functions > utils. Copie e cole o seguinte código nesse arquivo:
// auth.js
const { admin, db } = require('./admin');
module.exports = (request, response, next) => {
let idToken;
if (request.headers.authorization && request.headers.authorization.startsWith('Bearer ')) {
idToken = request.headers.authorization.split('Bearer ')[1];
} else {
console.error('No token found');
return response.status(403).json({ error: 'Unauthorized' });
}
admin
.auth()
.verifyIdToken(idToken)
.then((decodedToken) => {
request.user = decodedToken;
return db.collection('users').where('userId', '==', request.user.uid).limit(1).get();
})
.then((data) => {
request.user.username = data.docs[0].data().username;
request.user.imageUrl = data.docs[0].data().imageUrl;
return next();
})
.catch((err) => {
console.error('Error while verifying token', err);
return response.status(403).json(err);
});
};
Aqui, estamos usando o módulo verifyIdToken do Firebase para verificar o token. Depois disso, estaremos decodificando os detalhes do usuário e enviando-os na requisição existente.
Vá até users.js
e adicione o seguinte código abaixo do método signup
:
// users.js
deleteImage = (imageName) => {
const bucket = admin.storage().bucket();
const path = `${imageName}`
return bucket.file(path).delete()
.then(() => {
return
})
.catch((error) => {
return
})
}
// Upload profile picture
exports.uploadProfilePhoto = (request, response) => {
const BusBoy = require('busboy');
const path = require('path');
const os = require('os');
const fs = require('fs');
const busboy = new BusBoy({ headers: request.headers });
let imageFileName;
let imageToBeUploaded = {};
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
if (mimetype !== 'image/png' && mimetype !== 'image/jpeg') {
return response.status(400).json({ error: 'Wrong file type submited' });
}
const imageExtension = filename.split('.')[filename.split('.').length - 1];
imageFileName = `${request.user.username}.${imageExtension}`;
const filePath = path.join(os.tmpdir(), imageFileName);
imageToBeUploaded = { filePath, mimetype };
file.pipe(fs.createWriteStream(filePath));
});
deleteImage(imageFileName);
busboy.on('finish', () => {
admin
.storage()
.bucket()
.upload(imageToBeUploaded.filePath, {
resumable: false,
metadata: {
metadata: {
contentType: imageToBeUploaded.mimetype
}
}
})
.then(() => {
const imageUrl = `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}?alt=media`;
return db.doc(`/users/${request.user.username}`).update({
imageUrl
});
})
.then(() => {
return response.json({ message: 'Image uploaded successfully' });
})
.catch((error) => {
console.error(error);
return response.status(500).json({ error: error.code });
});
});
busboy.end(request.rawBody);
};
Com isso, nossa API de carregar foto de perfil está completa. Execute o comando firebase serve
e vá até o Postman. Crie uma requisição, selecione o método POST, adicione o URL e, na seção de conteúdo, selecione o tipo form-data.
A requisição está protegida. Você precisará enviar o token bearer também. Para enviar o token bearer, faça login novamente, caso ele tenha expirado. Depois disso, em Postman App > Authorization tab > Type > Bearer Token, cole o token na seção do token.
URL: http://localhost:5000/todoapp-<app-id>/<region-name>/api/user/image
METHOD: GET
Body: { REFER THE IMAGE down below }

Aperte o botão para enviar a requisição no Postman e você receberá o seguinte resultado:
{
"message": "Image uploaded successfully"
}
4. Obter os detalhes do usuário:
Aqui, estamos buscando os dados dos nossos usuários a partir do banco de dados. Vá até index.js
, importe o método getUserDetail e atribua a rota GET.
// index.js
const {
..,
getUserDetail
} = require('./APIs/users')
app.get('/user', auth, getUserDetail);
Agora, vá até users.js
e adicione o seguinte código depois do módulo uploadProfilePhoto
:
// users.js
exports.getUserDetail = (request, response) => {
let userData = {};
db
.doc(`/users/${request.user.username}`)
.get()
.then((doc) => {
if (doc.exists) {
userData.userCredentials = doc.data();
return response.json(userData);
}
})
.catch((error) => {
console.error(error);
return response.status(500).json({ error: error.code });
});
}
Estamos usando o módulo doc().get() do Firebase para retornar os detalhes do usuário. Com isso, nossa API de obter detalhes do usuário está completa. Execute o comando firebase serve
e vá até o Postman. Crie uma requisição, selecione o método GET, adicione o URL e o conteúdo.
A requisição está protegida. Então, você precisará enviar o token bearer também. Para enviar o token bearer, faça login novamente caso o token já tenha expirado.
URL: http://localhost:5000/todoapp-<app-id>/<region-name>/api/user
METHOD: GET
Aperte o botão de enviar requisição no Postman e você receberá o seguinte resultado:
{
"userCredentials": {
"phoneNumber": "........",
"email": "........",
"country": "........",
"userId": "........",
"username": "........",
"createdAt": "........",
"lastName": "........",
"firstName": "........"
}
}
5. Atualizar os detalhes do usuário:
Agora, vamos adicionar a funcionalidade para atualizar os detalhes do usuário. Vá até index.js
, copie e cole o seguinte código:
// index.js
const {
..,
updateUserDetails
} = require('./APIs/users')
app.post('/user', auth, updateUserDetails);
Vá até users.js
e adicione o módulo updateUserDetails
abaixo do getUserDetails
existente:
// users.js
exports.updateUserDetails = (request, response) => {
let document = db.collection('users').doc(`${request.user.username}`);
document.update(request.body)
.then(()=> {
response.json({message: 'Updated successfully'});
})
.catch((error) => {
console.error(error);
return response.status(500).json({
message: "Cannot Update the value"
});
});
}
Aqui, estamos usando o método update do Firebase. Com isso, nossa API de atualizar os detalhes do usuário está completa. Siga o mesmo processo feito na API de obter detalhes do usuário para realizar uma requisição, com apenas uma mudança. Adicione o conteúdo na requisição e um método POST.
URL: http://localhost:5000/todoapp-<app-id>/<region-name>/api/user
METHOD: POST
Body : {
// You can edit First Name, last Name and country
// We will disable other Form Tags from our UI
}
Aperte o botão para enviar a requisição no Postman e você receberá o seguinte resultado:
{
"message": "Updated successfully"
}
6. Tornar a API de afazeres segura:
Para tornar a API de afazeres segura, de modo que apenas o usuário escolhido possa acessá-la, faremos algumas pequenas mudanças em nosso código atual. Primeiramente, vamos atualizar index.js
para:
// index.js
// Todos
app.get('/todos', auth, getAllTodos);
app.get('/todo/:todoId', auth, getOneTodo);
app.post('/todo',auth, postOneTodo);
app.delete('/todo/:todoId',auth, deleteTodo);
app.put('/todo/:todoId',auth, editTodo);
Atualizamos todas as rotas de afazeres adicionando auth
. Assim, todas as chamadas para a API precisarão de um token e poderão ser acessadas apenas por um usuário em particular.
Depois disso, vá até todos.js
no diretório functions > APIs.
- Crie a API de afazeres: abra
todos.js
e, no método postOneTodo, adicione a chave username, assim:
const newTodoItem = {
..,
username: request.user.username,
..
}
2. API para obter todos os afazeres: abra todos.js
e, no método getAllTodos, adicione a instrução where, assim:
db
.collection('todos')
.where('username', '==', request.user.username)
.orderBy('createdAt', 'desc')
Execute firebase serve
e teste nossa API GET. Não se esqueça de mandar o token bearer. Aqui, você terá uma resposta de erro assim:
{
"error": 9
}
Vá até a linha de comando e você verá as seguintes linhas:
i functions: Beginning execution of "api"> Error: 9 FAILED_PRECONDITION: The query requires an index. You can create it here: <URL>> at callErrorFromStatus
Abra o <URL> acima e clique em Create index.

Quando o índice estiver pronto, envie a requisição novamente e você terá o seguinte resultado:
[
{
"todoId": "......",
"title": "......",
"username": "......",
"body": "......",
"createdAt": "2020-03-30T13:01:58.478Z"
}
]
3. API para excluir um afazer: abra todos.js
e, abaixo de deleteTodo, adicione a condição abaixo. Adicione esta condição dentro da pesquisa document.get().then(), abaixo da condição !doc.exists.
..
if(doc.data().username !== request.user.username){
return response.status(403).json({error:"UnAuthorized"})
}
Estrutura do diretório até agora:
+-- firebase.json
+-- functions
| +-- API
| +-- +-- todos.js
| +-- +-- users.js
| +-- util
| +-- +-- admin.js
| +-- +-- auth.js
| +-- +-- validators.js
| +-- index.js
| +-- node_modules
| +-- package-lock.json
| +-- package.json
| +-- .gitignore
Com isso, completamos o back-end da nossa API. Faça uma pausa, tome um café e, depois disso, vamos começar a construir o front-end da nossa aplicação.
Seção 3: painel do usuário
Nesta seção, vamos desenvolver estes componentes:
- Configurar ReactJS e Material UI.
- Criar o formulário de login e registro.
- Criar a seção das contas.
O código do painel do usuário implementado nesta seção pode ser encontrado neste commit.
1. Configurar o ReactJS e o Material UI:
Vamos usar o template create-react-app. Ele nos dará uma estrutura fundamental para desenvolver a aplicação. Para instalá-lo, use o seguinte comando:
npm install -g create-react-app
Vá até a pasta da raiz do projeto onde se encontra o diretório functions. Inicialize o front-end da nossa aplicação usando o seguinte comando:
create-react-app view
Lembre-se de usar a versão v16.13.1 da biblioteca ReactJS.
Quando a instalação completar, você verá o seguinte nos registros da sua linha de comando:
cd view
npm start
Happy hacking!
Com isso, configuramos nossa aplicação do React. Você terá a seguinte estrutura de projeto:
+-- firebase.json
+-- functions { This Directory consists our API logic }
+-- view { This Directory consists our FrontEnd Compoenents }
+-- .firebaserc
+-- .gitignore
Agora, execute a aplicação usando o comando npm start
. Vá até http://localhost:3000/
no navegador e você verá o seguinte resultado:

Agora, vamos remover todos os componentes que não são necessários. Vá até o diretório view e remova todos os arquivos que tem a palavra [ Remove ] ao lado. Para isso, veja a árvore de estrutura do diretório abaixo.
+-- README.md [ Remove ]
+-- package-lock.json
+-- package.json
+-- node_modules
+-- .gitignore
+-- public
| +-- favicon.ico [ Remove ]
| +-- index.html
| +-- logo192.png [ Remove ]
| +-- logo512.png [ Remove ]
| +-- manifest.json
| +-- robots.txt
+-- src
| +-- App.css
| +-- App.test.js
| +-- index.js
| +-- serviceWorker.js
| +-- App.js
| +-- index.css [ Remove ]
| +-- logo.svg [ Remove ]
| +-- setupTests.js
Vá até index.html
, no diretório public, e remova as seguintes linhas:
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
Vá até App.js
, no diretório src e substitua o código antigo pelo seguinte código:
import React from 'react';
function App() {
return (
<div>
</div>
);
}
export default App;
Vá até index.js
e remova a seguinte importação:
import './index.css'
Não apaguei o App.css
nem o estou usando nesta aplicação. Porém, se você quiser apagá-lo ou usá-lo, fique à vontade.
Vá até http://localhost:3000/ no navegador e você verá uma tela branca como resultado.
Para instalar o Material UI, vá até o diretório view, copie e cole este comando no terminal:
npm install @material-ui/core
Lembre-se de usar a versão v4.9.8 da biblioteca Material UI.
2. Formulário de login:
Para desenvolver o formulário de login, vá até App.js
. No topo do arquivo App.js
, adicione as seguintes importações:
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import login from './pages/login';
Estamos usando Switch e Route para atribuir rotas para nossa aplicação de afazeres. Agora, vamos adicionar apenas a rota de /login e atribuir um componente de login a ela.
// App.js
<Router>
<div>
<Switch>
<Route exact path="/login" component={login}/>
</Switch>
</div>
</Router>
Crie um diretório pages abaixo do diretório view existente e um arquivo chamado login.js
no diretório pages.
Vamos importar os componentes do Material UI e o pacote Axios em login.js
:
// login.js
// Material UI components
import React, { Component } from 'react';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
import Typography from '@material-ui/core/Typography';
import withStyles from '@material-ui/core/styles/withStyles';
import Container from '@material-ui/core/Container';
import CircularProgress from '@material-ui/core/CircularProgress';
import axios from 'axios';
Vamos adicionar a seguinte estilização para nossa página de login:
// login.js
const styles = (theme) => ({
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
form: {
width: '100%',
marginTop: theme.spacing(1)
},
submit: {
margin: theme.spacing(3, 0, 2)
},
customError: {
color: 'red',
fontSize: '0.8rem',
marginTop: 10
},
progess: {
position: 'absolute'
}
});
Vamos criar uma classe chamada login, que possui um formulário e uma função que lida com o envio desse formulário.
// login.js
class login extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
errors: [],
loading: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.UI.errors) {
this.setState({
errors: nextProps.UI.errors
});
}
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({ loading: true });
const userData = {
email: this.state.email,
password: this.state.password
};
axios
.post('/login', userData)
.then((response) => {
localStorage.setItem('AuthToken', `Bearer ${response.data.token}`);
this.setState({
loading: false,
});
this.props.history.push('/');
})
.catch((error) => {
this.setState({
errors: error.response.data,
loading: false
});
});
};
render() {
const { classes } = this.props;
const { errors, loading } = this.state;
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Login
</Typography>
<form className={classes.form} noValidate>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
helperText={errors.email}
error={errors.email ? true : false}
onChange={this.handleChange}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
helperText={errors.password}
error={errors.password ? true : false}
onChange={this.handleChange}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={this.handleSubmit}
disabled={loading || !this.state.email || !this.state.password}
>
Sign In
{loading && <CircularProgress size={30} className={classes.progess} />}
</Button>
<Grid container>
<Grid item>
<Link href="signup" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
{errors.general && (
<Typography variant="body2" className={classes.customError}>
{errors.general}
</Typography>
)}
</form>
</div>
</Container>
);
}
}
No fim desse arquivo, adicione a seguinte exportação:
export default withStyles(styles)(login);
Adicione o URL das nossas funções do Firebase em view > package.json assim:
Lembre-se de adicionar uma chave chamada proxy abaixo do objeto de JSON browserslist
"proxy": "https://<nome-da-região>-todoapp-<id>.cloudfunctions.net/api"
Instale os pacotes Axios e Material Icons usando os seguintes comandos:
// Axios command:
npm i axios
// Material Icons:
npm install @material-ui/icons
Adicionamos a rota login em App.js
. No arquivo login.js
, criamos um componente de classe que lida com o estado e envia a requisição POST para a API de login usando o pacote Axios. Se a requisição for bem-sucedida, armazenamos o token. Se tivermos erros na resposta, simplesmente mostramos os erros na interface.
Vá até http://localhost:3000/login no navegador e você verá a seguinte interface de login.

Tente preencher credenciais erradas ou enviar uma requisição vazia e você terá erros. Envie uma requisição válida. Vá até Developer console > Application. Você verá que o token dos usuários está armazenado no armazenamento local. Quando o login for feito com sucesso, vamos ser redirecionados para a página de início.

3. Formulário de registro:
Para desenvolver o formulário de registro, vá até App.js
e atualize o componente Route
existente com a linha abaixo:
// App.js
<Route exact path="/signup" component={signup}/>
Não se esqueça de importar:
// App.js
import signup from './pages/signup';
Crie um arquivo chamado signup.js
no diretório pages.
Dentro de signup.js
, vamos importar os pacotes Material UI e Axios:
// signup.js
import React, { Component } from 'react';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
import Typography from '@material-ui/core/Typography';
import Container from '@material-ui/core/Container';
import withStyles from '@material-ui/core/styles/withStyles';
import CircularProgress from '@material-ui/core/CircularProgress';
import axios from 'axios';
Vamos adicionar a seguinte estilização em nossa página de registro:
// signup.js
const styles = (theme) => ({
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(3)
},
submit: {
margin: theme.spacing(3, 0, 2)
},
progess: {
position: 'absolute'
}
});
Vamos criar uma classe chamada signup, que possui um formulário e uma função que lida com o envio dele.
// signup.js
class signup extends Component {
constructor(props) {
super(props);
this.state = {
firstName: '',
lastName: '',
phoneNumber: '',
country: '',
username: '',
email: '',
password: '',
confirmPassword: '',
errors: [],
loading: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.UI.errors) {
this.setState({
errors: nextProps.UI.errors
});
}
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({ loading: true });
const newUserData = {
firstName: this.state.firstName,
lastName: this.state.lastName,
phoneNumber: this.state.phoneNumber,
country: this.state.country,
username: this.state.username,
email: this.state.email,
password: this.state.password,
confirmPassword: this.state.confirmPassword
};
axios
.post('/signup', newUserData)
.then((response) => {
localStorage.setItem('AuthToken', `${response.data.token}`);
this.setState({
loading: false,
});
this.props.history.push('/');
})
.catch((error) => {
this.setState({
errors: error.response.data,
loading: false
});
});
};
render() {
const { classes } = this.props;
const { errors, loading } = this.state;
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="firstName"
label="First Name"
name="firstName"
autoComplete="firstName"
helperText={errors.firstName}
error={errors.firstName ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="lastName"
label="Last Name"
name="lastName"
autoComplete="lastName"
helperText={errors.lastName}
error={errors.lastName ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="username"
label="User Name"
name="username"
autoComplete="username"
helperText={errors.username}
error={errors.username ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="phoneNumber"
label="Phone Number"
name="phoneNumber"
autoComplete="phoneNumber"
pattern="[7-9]{1}[0-9]{9}"
helperText={errors.phoneNumber}
error={errors.phoneNumber ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
helperText={errors.email}
error={errors.email ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="country"
label="Country"
name="country"
autoComplete="country"
helperText={errors.country}
error={errors.country ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
helperText={errors.password}
error={errors.password ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="confirmPassword"
label="Confirm Password"
type="password"
id="confirmPassword"
autoComplete="current-password"
onChange={this.handleChange}
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={this.handleSubmit}
disabled={loading ||
!this.state.email ||
!this.state.password ||
!this.state.firstName ||
!this.state.lastName ||
!this.state.country ||
!this.state.username ||
!this.state.phoneNumber}
>
Sign Up
{loading && <CircularProgress size={30} className={classes.progess} />}
</Button>
<Grid container justify="flex-end">
<Grid item>
<Link href="login" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</form>
</div>
</Container>
);
}
}
No fim desse arquivo, adicione a seguinte exportação:
export default withStyles(styles)(signup);
A lógica para o componente Signup é a mesma do componente de login. Vá até http://localhost:3000/signup no navegador e você verá a interface de registro abaixo. Quando o registro for feito com sucesso, seremos redirecionados para a página inicial.

Tente preencher credenciais erradas ou enviar uma requisição vazia e você terá erros. Envie uma requisição válida. Vá até Developer console > Application. Você verá que o token dos usuários está armazenado no armazenamento local.

4. Seção das contas:
Para construir a página de contas, vamos precisar primeiro criar nossa página inicial, que será onde carregaremos a seção da conta. Vá até App.js
e atualize a seguinte rota:
// App.js
<Route exact path="/" component={home}/>
Não se esqueça de importar:
// App.js
import home from './pages/home';
Crie um arquivo chamado home.js
. Esse arquivo será o índice da nossa aplicação. As seções sobre Contas e Afazeres serão carregadas com base nessa página no clique do botão.
Importe os pacotes do Material UI, o pacote Axios, nossa Conta personalizada, componentes de afazeres e middleware de autenticação.
// home.js
import React, { Component } from 'react';
import axios from 'axios';
import Account from '../components/account';
import Todo from '../components/todo';
import Drawer from '@material-ui/core/Drawer';
import AppBar from '@material-ui/core/AppBar';
import CssBaseline from '@material-ui/core/CssBaseline';
import Toolbar from '@material-ui/core/Toolbar';
import List from '@material-ui/core/List';
import Typography from '@material-ui/core/Typography';
import Divider from '@material-ui/core/Divider';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import withStyles from '@material-ui/core/styles/withStyles';
import AccountBoxIcon from '@material-ui/icons/AccountBox';
import NotesIcon from '@material-ui/icons/Notes';
import Avatar from '@material-ui/core/avatar';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import CircularProgress from '@material-ui/core/CircularProgress';
import { authMiddleWare } from '../util/auth'
Vamos definir nosso drawerWidth assim:
const drawerWidth = 240;
Vamos adicionar a seguinte estilização na nossa página inicial:
const styles = (theme) => ({
root: {
display: 'flex'
},
appBar: {
zIndex: theme.zIndex.drawer + 1
},
drawer: {
width: drawerWidth,
flexShrink: 0
},
drawerPaper: {
width: drawerWidth
},
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
avatar: {
height: 110,
width: 100,
flexShrink: 0,
flexGrow: 0,
marginTop: 20
},
uiProgess: {
position: 'fixed',
zIndex: '1000',
height: '31px',
width: '31px',
left: '50%',
top: '35%'
},
toolbar: theme.mixins.toolbar
});
Vamos criar uma classe chamada home. Essa classe terá uma chamada de API para obter a foto de perfil do usuário, primeiro e último nome. Também terá a lógica para escolher qual componente mostrar, se o dos Afazeres ou o das Contas:
class home extends Component {
state = {
render: false
};
loadAccountPage = (event) => {
this.setState({ render: true });
};
loadTodoPage = (event) => {
this.setState({ render: false });
};
logoutHandler = (event) => {
localStorage.removeItem('AuthToken');
this.props.history.push('/login');
};
constructor(props) {
super(props);
this.state = {
firstName: '',
lastName: '',
profilePicture: '',
uiLoading: true,
imageLoading: false
};
}
componentWillMount = () => {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.get('/user')
.then((response) => {
console.log(response.data);
this.setState({
firstName: response.data.userCredentials.firstName,
lastName: response.data.userCredentials.lastName,
email: response.data.userCredentials.email,
phoneNumber: response.data.userCredentials.phoneNumber,
country: response.data.userCredentials.country,
username: response.data.userCredentials.username,
uiLoading: false,
profilePicture: response.data.userCredentials.imageUrl
});
})
.catch((error) => {
if(error.response.status === 403) {
this.props.history.push('/login')
}
console.log(error);
this.setState({ errorMsg: 'Error in retrieving the data' });
});
};
render() {
const { classes } = this.props;
if (this.state.uiLoading === true) {
return (
<div className={classes.root}>
{this.state.uiLoading && <CircularProgress size={150} className={classes.uiProgess} />}
</div>
);
} else {
return (
<div className={classes.root}>
<CssBaseline />
<AppBar position="fixed" className={classes.appBar}>
<Toolbar>
<Typography variant="h6" noWrap>
TodoApp
</Typography>
</Toolbar>
</AppBar>
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
paper: classes.drawerPaper
}}
>
<div className={classes.toolbar} />
<Divider />
<center>
<Avatar src={this.state.profilePicture} className={classes.avatar} />
<p>
{' '}
{this.state.firstName} {this.state.lastName}
</p>
</center>
<Divider />
<List>
<ListItem button key="Todo" onClick={this.loadTodoPage}>
<ListItemIcon>
{' '}
<NotesIcon />{' '}
</ListItemIcon>
<ListItemText primary="Todo" />
</ListItem>
<ListItem button key="Account" onClick={this.loadAccountPage}>
<ListItemIcon>
{' '}
<AccountBoxIcon />{' '}
</ListItemIcon>
<ListItemText primary="Account" />
</ListItem>
<ListItem button key="Logout" onClick={this.logoutHandler}>
<ListItemIcon>
{' '}
<ExitToAppIcon />{' '}
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>
</List>
</Drawer>
<div>{this.state.render ? <Account /> : <Todo />}</div>
</div>
);
}
}
}
Aqui no código, você verá que authMiddleWare(this.props.history);
é usado. Esse middleware verifica se authToken
é nulo. Se for, ele direcionará o usuário de volta para login.js
. Isso foi adicionado para que nosso usuário não possa acessar a rota /
sem se registrar ou realizar login. No fim desse arquivo, adicione a seguinte exportação:
export default withStyles(styles)(home);
Agora, você deve estar se perguntando o que este código em home.js
faz?
<div>{this.state.render ? <Account /> : <Todo />}</div>
Ele está verificando o estado de renderização que estamos definindo no clique do botão. Vamos criar o diretório component e, nesse diretório, criaremos dois arquivos: account.js
e todo.js
.
Vamos criar um diretório chamado util e um arquivo chamado auth.js
nesse diretório. Copie e cole o seguinte código em auth.js
:
export const authMiddleWare = (history) => {
const authToken = localStorage.getItem('AuthToken');
if(authToken === null){
history.push('/login')
}
}
Por agora, dentro do arquivo todo.js
, vamos apenas escrever uma classe que renderiza o texto Hello I am todo. Trabalharemos em nossos afazeres na próxima seção:
import React, { Component } from 'react'
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
const styles = ((theme) => ({
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
toolbar: theme.mixins.toolbar,
})
);
class todo extends Component {
render() {
const { classes } = this.props;
return (
<main className={classes.content}>
<div className={classes.toolbar} />
<Typography paragraph>
Hello I am todo
</Typography>
</main>
)
}
}
export default (withStyles(styles)(todo));
Agora, é hora de fazer a seção das contas. Importe o Material UI, o clsx, o axios e o authmiddleWare em account.js
.
// account.js
import React, { Component } from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import CircularProgress from '@material-ui/core/CircularProgress';
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
import { Card, CardActions, CardContent, Divider, Button, Grid, TextField } from '@material-ui/core';
import clsx from 'clsx';
import axios from 'axios';
import { authMiddleWare } from '../util/auth';
Vamos adicionar a seguinte estilização à nossa página de contas:
// account.js
const styles = (theme) => ({
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
toolbar: theme.mixins.toolbar,
root: {},
details: {
display: 'flex'
},
avatar: {
height: 110,
width: 100,
flexShrink: 0,
flexGrow: 0
},
locationText: {
paddingLeft: '15px'
},
buttonProperty: {
position: 'absolute',
top: '50%'
},
uiProgess: {
position: 'fixed',
zIndex: '1000',
height: '31px',
width: '31px',
left: '50%',
top: '35%'
},
progess: {
position: 'absolute'
},
uploadButton: {
marginLeft: '8px',
margin: theme.spacing(1)
},
customError: {
color: 'red',
fontSize: '0.8rem',
marginTop: 10
},
submitButton: {
marginTop: '10px'
}
});
Vamos criar um componente de classe chamado account. Por agora, apenas copie e cole o seguinte código:
// account.js
class account extends Component {
constructor(props) {
super(props);
this.state = {
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
username: '',
country: '',
profilePicture: '',
uiLoading: true,
buttonLoading: false,
imageError: ''
};
}
componentWillMount = () => {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.get('/user')
.then((response) => {
console.log(response.data);
this.setState({
firstName: response.data.userCredentials.firstName,
lastName: response.data.userCredentials.lastName,
email: response.data.userCredentials.email,
phoneNumber: response.data.userCredentials.phoneNumber,
country: response.data.userCredentials.country,
username: response.data.userCredentials.username,
uiLoading: false
});
})
.catch((error) => {
if (error.response.status === 403) {
this.props.history.push('/login');
}
console.log(error);
this.setState({ errorMsg: 'Error in retrieving the data' });
});
};
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
handleImageChange = (event) => {
this.setState({
image: event.target.files[0]
});
};
profilePictureHandler = (event) => {
event.preventDefault();
this.setState({
uiLoading: true
});
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
let form_data = new FormData();
form_data.append('image', this.state.image);
form_data.append('content', this.state.content);
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.post('/user/image', form_data, {
headers: {
'content-type': 'multipart/form-data'
}
})
.then(() => {
window.location.reload();
})
.catch((error) => {
if (error.response.status === 403) {
this.props.history.push('/login');
}
console.log(error);
this.setState({
uiLoading: false,
imageError: 'Error in posting the data'
});
});
};
updateFormValues = (event) => {
event.preventDefault();
this.setState({ buttonLoading: true });
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
const formRequest = {
firstName: this.state.firstName,
lastName: this.state.lastName,
country: this.state.country
};
axios
.post('/user', formRequest)
.then(() => {
this.setState({ buttonLoading: false });
})
.catch((error) => {
if (error.response.status === 403) {
this.props.history.push('/login');
}
console.log(error);
this.setState({
buttonLoading: false
});
});
};
render() {
const { classes, ...rest } = this.props;
if (this.state.uiLoading === true) {
return (
<main className={classes.content}>
<div className={classes.toolbar} />
{this.state.uiLoading && <CircularProgress size={150} className={classes.uiProgess} />}
</main>
);
} else {
return (
<main className={classes.content}>
<div className={classes.toolbar} />
<Card {...rest} className={clsx(classes.root, classes)}>
<CardContent>
<div className={classes.details}>
<div>
<Typography className={classes.locationText} gutterBottom variant="h4">
{this.state.firstName} {this.state.lastName}
</Typography>
<Button
variant="outlined"
color="primary"
type="submit"
size="small"
startIcon={<CloudUploadIcon />}
className={classes.uploadButton}
onClick={this.profilePictureHandler}
>
Upload Photo
</Button>
<input type="file" onChange={this.handleImageChange} />
{this.state.imageError ? (
<div className={classes.customError}>
{' '}
Wrong Image Format || Supported Format are PNG and JPG
</div>
) : (
false
)}
</div>
</div>
<div className={classes.progress} />
</CardContent>
<Divider />
</Card>
<br />
<Card {...rest} className={clsx(classes.root, classes)}>
<form autoComplete="off" noValidate>
<Divider />
<CardContent>
<Grid container spacing={3}>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="First name"
margin="dense"
name="firstName"
variant="outlined"
value={this.state.firstName}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Last name"
margin="dense"
name="lastName"
variant="outlined"
value={this.state.lastName}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Email"
margin="dense"
name="email"
variant="outlined"
disabled={true}
value={this.state.email}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Phone Number"
margin="dense"
name="phone"
type="number"
variant="outlined"
disabled={true}
value={this.state.phoneNumber}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="User Name"
margin="dense"
name="userHandle"
disabled={true}
variant="outlined"
value={this.state.username}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Country"
margin="dense"
name="country"
variant="outlined"
value={this.state.country}
onChange={this.handleChange}
/>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions />
</form>
</Card>
<Button
color="primary"
variant="contained"
type="submit"
className={classes.submitButton}
onClick={this.updateFormValues}
disabled={
this.state.buttonLoading ||
!this.state.firstName ||
!this.state.lastName ||
!this.state.country
}
>
Save details
{this.state.buttonLoading && <CircularProgress size={30} className={classes.progess} />}
</Button>
</main>
);
}
}
}
No fim desse arquivo, adicione a seguinte exportação:
export default withStyles(styles)(account);
Em account.js
, há vários componentes sendo usados. Vamos ver como está nossa aplicação. Depois disso, vamos explicar todos os componentes que estão sendo usados e o motivo de estarem sendo usados.
Vá até o navegador. Se o token expirou, ele redirecionará você para a página de login
. Adicione seus detalhes e faça login novamente. Quando fizer isso, vá até a aba Account
e você verá a seguinte interface:

Há 3 funções que lidam com questões diferentes na seção das contas:
- componentWillMount: este é o método incorporado ao ciclo de vida do React. Estamos usando esse método para carregar os dados antes do ciclo de vida de renderização e para atualizar nossos valores do estado.
- ProfilePictureUpdate: esta é nossa função personalizada que estamos usando para lidar com o clique do usuário no botão de carregar foto. Ela enviará os dados para um servidor e recarregará a página para mostrar a nova foto de perfil do usuário.
- updateFormValues: esta também é nossa função personalizada que lida com a atualização dos detalhes do usuário. Aqui, o usuário pode atualizar seu primeiro nome, sobrenome e país. Não estamos permitindo a atualização de e-mail e nome de usuário, pois nossa lógica do back-end depende dessas chaves.
Além dessas três funções, há uma página de formulário estilizada. Aqui está a estrutura do diretório até aqui dentro da pasta view:
+-- public
+-- src
| +-- components
| +-- +-- todo.js
| +-- +-- account.js
| +-- pages
| +-- +-- home.js
| +-- +-- login.js
| +-- +-- signup.js
| +-- util
| +-- +-- auth.js
| +-- README.md
| +-- package-lock.json
| +-- package.json
| +-- .gitignore
Com isso, completamos nosso painel de contas. Agora tome um café, faça uma pausa e, na próxima seção, vamos criar o painel de afazeres.
Seção 4: painel de afazeres
Nesta seção, vamos desenvolver a interface para essas funcionalidades do painel de afazeres:
- Adicionar um afazer
- Obter todos os afazeres
- Excluir um afazer
- Editar um afazer
- Obter um afazer
- Aplicar o tema
O código do painel de afazeres implementado nesta seção pode ser encontrado neste commit.
Vá até todos.js
no diretório components. Adicione as seguintes importações às importações existentes:
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Slide from '@material-ui/core/Slide';
import TextField from '@material-ui/core/TextField';
import Grid from '@material-ui/core/Grid';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CircularProgress from '@material-ui/core/CircularProgress';
import CardContent from '@material-ui/core/CardContent';
import MuiDialogTitle from '@material-ui/core/DialogTitle';
import MuiDialogContent from '@material-ui/core/DialogContent';
import axios from 'axios';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { authMiddleWare } from '../util/auth';
Precisamos, também, adicionar os seguintes elementos do CSS nos componentes de estilo abaixo:
const styles = (theme) => ({
.., // Existing CSS elements
title: {
marginLeft: theme.spacing(2),
flex: 1
},
submitButton: {
display: 'block',
color: 'white',
textAlign: 'center',
position: 'absolute',
top: 14,
right: 10
},
floatingButton: {
position: 'fixed',
bottom: 0,
right: 0
},
form: {
width: '98%',
marginLeft: 13,
marginTop: theme.spacing(3)
},
toolbar: theme.mixins.toolbar,
root: {
minWidth: 470
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)'
},
pos: {
marginBottom: 12
},
uiProgess: {
position: 'fixed',
zIndex: '1000',
height: '31px',
width: '31px',
left: '50%',
top: '35%'
},
dialogeStyle: {
maxWidth: '50%'
},
viewRoot: {
margin: 0,
padding: theme.spacing(2)
},
closeButton: {
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500]
}
});
Vamos adicionar a transição para o pop-up da caixa de diálogo:
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
Remova a classe todo abaixo e copie e cole a seguinte classe:
class todo extends Component {
constructor(props) {
super(props);
this.state = {
todos: '',
title: '',
body: '',
todoId: '',
errors: [],
open: false,
uiLoading: true,
buttonType: '',
viewOpen: false
};
this.deleteTodoHandler = this.deleteTodoHandler.bind(this);
this.handleEditClickOpen = this.handleEditClickOpen.bind(this);
this.handleViewOpen = this.handleViewOpen.bind(this);
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
componentWillMount = () => {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.get('/todos')
.then((response) => {
this.setState({
todos: response.data,
uiLoading: false
});
})
.catch((err) => {
console.log(err);
});
};
deleteTodoHandler(data) {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
let todoId = data.todo.todoId;
axios
.delete(`todo/${todoId}`)
.then(() => {
window.location.reload();
})
.catch((err) => {
console.log(err);
});
}
handleEditClickOpen(data) {
this.setState({
title: data.todo.title,
body: data.todo.body,
todoId: data.todo.todoId,
buttonType: 'Edit',
open: true
});
}
handleViewOpen(data) {
this.setState({
title: data.todo.title,
body: data.todo.body,
viewOpen: true
});
}
render() {
const DialogTitle = withStyles(styles)((props) => {
const { children, classes, onClose, ...other } = props;
return (
<MuiDialogTitle disableTypography className={classes.root} {...other}>
<Typography variant="h6">{children}</Typography>
{onClose ? (
<IconButton aria-label="close" className={classes.closeButton} onClick={onClose}>
<CloseIcon />
</IconButton>
) : null}
</MuiDialogTitle>
);
});
const DialogContent = withStyles((theme) => ({
viewRoot: {
padding: theme.spacing(2)
}
}))(MuiDialogContent);
dayjs.extend(relativeTime);
const { classes } = this.props;
const { open, errors, viewOpen } = this.state;
const handleClickOpen = () => {
this.setState({
todoId: '',
title: '',
body: '',
buttonType: '',
open: true
});
};
const handleSubmit = (event) => {
authMiddleWare(this.props.history);
event.preventDefault();
const userTodo = {
title: this.state.title,
body: this.state.body
};
let options = {};
if (this.state.buttonType === 'Edit') {
options = {
url: `/todo/${this.state.todoId}`,
method: 'put',
data: userTodo
};
} else {
options = {
url: '/todo',
method: 'post',
data: userTodo
};
}
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios(options)
.then(() => {
this.setState({ open: false });
window.location.reload();
})
.catch((error) => {
this.setState({ open: true, errors: error.response.data });
console.log(error);
});
};
const handleViewClose = () => {
this.setState({ viewOpen: false });
};
const handleClose = (event) => {
this.setState({ open: false });
};
if (this.state.uiLoading === true) {
return (
<main className={classes.content}>
<div className={classes.toolbar} />
{this.state.uiLoading && <CircularProgress size={150} className={classes.uiProgess} />}
</main>
);
} else {
return (
<main className={classes.content}>
<div className={classes.toolbar} />
<IconButton
className={classes.floatingButton}
color="primary"
aria-label="Add Todo"
onClick={handleClickOpen}
>
<AddCircleIcon style={{ fontSize: 60 }} />
</IconButton>
<Dialog fullScreen open={open} onClose={handleClose} TransitionComponent={Transition}>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton edge="start" color="inherit" onClick={handleClose} aria-label="close">
<CloseIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{this.state.buttonType === 'Edit' ? 'Edit Todo' : 'Create a new Todo'}
</Typography>
<Button
autoFocus
color="inherit"
onClick={handleSubmit}
className={classes.submitButton}
>
{this.state.buttonType === 'Edit' ? 'Save' : 'Submit'}
</Button>
</Toolbar>
</AppBar>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="todoTitle"
label="Todo Title"
name="title"
autoComplete="todoTitle"
helperText={errors.title}
value={this.state.title}
error={errors.title ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="todoDetails"
label="Todo Details"
name="body"
autoComplete="todoDetails"
multiline
rows={25}
rowsMax={25}
helperText={errors.body}
error={errors.body ? true : false}
onChange={this.handleChange}
value={this.state.body}
/>
</Grid>
</Grid>
</form>
</Dialog>
<Grid container spacing={2}>
{this.state.todos.map((todo) => (
<Grid item xs={12} sm={6}>
<Card className={classes.root} variant="outlined">
<CardContent>
<Typography variant="h5" component="h2">
{todo.title}
</Typography>
<Typography className={classes.pos} color="textSecondary">
{dayjs(todo.createdAt).fromNow()}
</Typography>
<Typography variant="body2" component="p">
{`${todo.body.substring(0, 65)}`}
</Typography>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={() => this.handleViewOpen({ todo })}>
{' '}
View{' '}
</Button>
<Button size="small" color="primary" onClick={() => this.handleEditClickOpen({ todo })}>
Edit
</Button>
<Button size="small" color="primary" onClick={() => this.deleteTodoHandler({ todo })}>
Delete
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
<Dialog
onClose={handleViewClose}
aria-labelledby="customized-dialog-title"
open={viewOpen}
fullWidth
classes={{ paperFullWidth: classes.dialogeStyle }}
>
<DialogTitle id="customized-dialog-title" onClose={handleViewClose}>
{this.state.title}
</DialogTitle>
<DialogContent dividers>
<TextField
fullWidth
id="todoDetails"
name="body"
multiline
readonly
rows={1}
rowsMax={25}
value={this.state.body}
InputProps={{
disableUnderline: true
}}
/>
</DialogContent>
</Dialog>
</main>
);
}
}
}
No fim desse arquivo, adicione a seguinte exportação:
export default withStyles(styles)(todo);
Primeiro, vamos entender como nossa interface funciona e, depois disso, vamos entender o código. Vá até o navegador e você verá a seguinte interface:

Clique no botão Add, no canto inferior direito, e verá a seguinte tela:

Adicione o título do afazer e os detalhes. Depois, pressione o botão de envio. Você verá a seguinte tela:

Depois desse clique no botão de visualização, você poderá ver os detalhes completos do afazer:

Clique no botão Edit e você poderá editar o afazer:

Clique no botão de exclusão e você poderá excluir o afazer. Agora que sabemos como o painel funciona, entenderemos os componentes usados.
1. Adicionar afazer: para adicionar um afazer, usaremos o componente Dialogue do Material UI. Esse componente implementa a funcionalidade hook. Usamos classes. Por isso, removeremos essa funcionalidade.
// This sets the state to open and buttonType flag to add:
const handleClickOpen = () => {
this.setState({
todoId: '',
title: '',
body: '',
buttonType: '',
open: true
});
};
// This sets the state to close:
const handleClose = (event) => {
this.setState({ open: false });
};
Além disso, também mudaremos a posição do botão de adicionar afazeres.
// Position our button
floatingButton: {
position: 'fixed',
bottom: 0,
right: 0
},
<IconButton className={classes.floatingButton} ... >
Agora, substituiremos a tag list por form dentro deste Dialogue. Isso nos ajudará a adicionar o novo afazer.
// Show Edit or Save depending on buttonType state
{this.state.buttonType === 'Edit' ? 'Save' : 'Submit'}
// Our Form to add a todo
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
// TextField here
</Grid>
<Grid item xs={12}>
// TextField here
</Grid>
</Grid>
</form>
A função handleSubmit consiste de lógica para realizar a leitura do estado buttonType
. Se o estado for uma string vazia ("")
, realizará um post na API de adicionar afazer. Se o estado for um Edit
, realizará um update na API editar afazer.
2. Obter afazeres: para mostrar os afazeres vamos usar o Grid container
e, dentro dele, colocaremos o Grid item
. Dentro desse item, vamos usar um componente Card
para mostrar os dados.
<Grid container spacing={2}>
{this.state.todos.map((todo) => (
<Grid item xs={12} sm={6}>
<Card className={classes.root} variant="outlined">
<CardContent>
// Here will show Todo with view, edit and delete button
</CardContent>
</Card>
</Grid>))}
</Grid>
Usamos a função map para mostrar um afazer de cada vez, à medida que a API envia os afazeres como uma lista. Vamos usar a função de ciclo de vida componentWillMount para obter e enviar o estado antes da renderização ser executada. Há três botões (view, edit e delete). Vamos precisar de três funções para lidar com a operação quando o botão for clicado. Vamos aprender sobre esses botões nas respectivas subseções.
3. Editar afazer: para editar o afazer, estamos reutilizando o código de pop-up de diálogo que está sendo usado para adicionar afazeres. Para diferenciar entre os cliques dos botões, usaremos um estado buttonType
. Para adicionar um afazer, o estado buttonType
é ("")
. Para editar um afazer, ele é Edit
.
handleEditClickOpen(data) {
this.setState({
..,
buttonType: 'Edit',
..
});
}
No método handleSubmit
, lemos o estado buttonType
e enviamos a requisição de acordo.
4. Excluir afazer: quando esse botão é clicado, enviamos o afazer como objeto para nosso deleteTodoHandler. Então, ele envia a requisição para o back-end.
<Button size="small" onClick={() => this.deleteTodoHandler({ todo })}>Delete</Button>
5. Visualizar afazer: quando mostramos os dados, mostramos apenas uma parte deles. Assim, o usuário terá uma ideia sobre o assunto do afazer. Se, no entanto, o usuário quiser saber mais sobre ele, é preciso clicar no botão de visualizar.
Para isso, vamos usar o diálogo customizado. Dentro dele, usamos DialogTitle e DialogContent. Isso mostra nosso título e conteúdo. No DialougeContent, vamos usar o formulário para mostrar o conteúdo que o usuário postou (essa é uma solução que encontrei – há muitas e você pode tentar a que quiser).
// This is used to remove the underline of the Form
InputProps={{
disableUnderline: true
}}
// This is used so that user cannot edit the data
readonly
6. Aplicar o tema: este é o último passo da nossa aplicação. Vamos aplicar um tema a ela. Para isso, usaremos createMuiTheme
e ThemeProvider
, do Material UI. Copie e cole o seguinte código em App.js
:
import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles';
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
const theme = createMuiTheme({
palette: {
primary: {
light: '#33c9dc',
main: '#FF5722',
dark: '#d50000',
contrastText: '#fff'
}
}
});
function App() {
return (
<MuiThemeProvider theme={theme}>
// Router and switch will be here.
</MuiThemeProvider>
);
}
Esquecemos de aplicar um tema para nosso botão de todo.js
, em CardActions
. Adicione a tag color
aos botões de visualização, edição e exclusão.
<Button size="small" color="primary" ...>
Vá até o navegador e você verá que tudo está igual. A única diferença é a cor da aplicação.

Assim, chegamos ao final do projeto! Criamos uma aplicação de afazeres (TodoApp) usando o ReactJS e o Firebase. Se chegou até aqui, meus parabéns por ter conseguido.
Fique à vontade para se conectar com o autor pelo Twitter e pelo Github.