A primeira coisa que precisamos fazer é inicializar um novo projeto com o npm e instalar o json-server:
npm init -y
npm install json-server
Vamos aproveitar e já criar também um script para executar o json-server:
// ...
"scripts": {
"serve": "json-server --watch db.json"
},
// ...
E também criar um arquivo db.json com os dados inicias do backend:
{
"transactions": [
{
"id": 1,
"name": "Freela",
"amount": 300
},
{
"name": "Salário",
"amount": 3200,
"id": 3
},
{
"name": "Conta de água",
"amount": -60,
"id": 4
},
{
"name": "Conta de luz",
"amount": -150,
"id": 5
}
]
}
Com essa preparação feita, vamos passar para a criação de uma página html com a estrutura da nossa aplicação:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minhas Finanças</title>
</head>
<body>
<h1>Minhas Finanças</h1>
<main>
<form>
<h2>Nova Transação</h2>
<input type="hidden" id="id" name="id">
<label for="name">Nome:</label>
<input type="text" id="name" name="name">
<label for="amount">Valor:</label>
<input type="number" step="0.01" id="amount" name="amount">
<br><br>
<button>Salvar</button>
</form>
<div>
<h2>Saldo: <span id="balance"></span></h2>
<h2>Histórico de Transações</h2>
<section id="transactions"></section>
</div>
</main>
<script src="index.js"></script>
</body>
</html>
Agora iniciaremos a parte do javascript. Eu irei criar primeiro algumas funções auxiliares para criação dos elementos do DOM, assim nossas funções ficarão menores e mais específicas. Vamos começar pela criação de um container para os dados da transação e o nome:
function createTransactionContainer(id) {
const container = document.createElement('div')
container.classList.add('transaction')
container.id = `transaction-${id}`
return container
}
function createTransactionTitle(name) {
const title = document.createElement('span')
title.classList.add('transaction-title')
title.textContent = name
return title
}
A próxima função é a que cria um elemento para o valor da transação. Para essa eu vou te mostrar como usar a API de internacionalização do navegador para formatar valores numéricos em moedas através do Intl.NumberFormat(). Iremos criar um span para o valor da transação e formatá-lo para a moeda brasileira (BRL), adicionando também uma diferenciação entre valores positivos (crédito) e valores negativos (débito):
// ...
function createTransactionAmount(amount) {
const span = document.createElement('span')
span.classList.add('transaction-amount')
const formater = Intl.NumberFormat('pt-BR', {
compactDisplay: 'long',
currency: 'BRL',
style: 'currency',
})
const formatedAmount = formater.format(amount)
if (amount > 0) {
span.textContent = `${formatedAmount} C`
span.classList.add('credit')
} else {
span.textContent = `${formatedAmount} D`
span.classList.add('debit')
}
return span
}
Vamos passar agora para a criação de uma função que renderiza uma transação na tela:
Obs.: repare como ela fica mais simples agora que já temos funções específicas para criação de cada elemento.
// ...
function renderTransaction(transaction) {
const container = createTransactionContainer(transaction.id)
const title = createTransactionTitle(transaction.name)
const amount = createTransactionAmount(transaction.amount)
document.querySelector('#transactions').append(container)
container.append(title, amount)
}
Vamos criar também uma função específica para fazer a requisição GET que obtém todas as transações do backend:
async function fetchTransactions() {
return await fetch('<http://localhost:3000/transactions>').then(res => res.json())
}
Para armazenar as transações vamos criar uma variável no escopo global que começará como um array vazio. Junto com ela criaremos uma função para exibir na tela (no elemento específico) o saldo total calculado com base nos valores de transações nesse array:
Obs.: repare que usando essa estratégia não precisaremos re-requisitar as transações atualizadas do backend a cada mundaça, podemos ir manipulando esse array a medida que elas são criadas/atualizadas/excluídas e assim economizar uso de banda e melhorar a performance da aplicação.
let transactions = []
// ...
function updateBalance() {
const balanceSpan = document.querySelector('#balance')
const balance = transactions.reduce((sum, transaction) => sum + transaction.amount, 0)
const formater = Intl.NumberFormat('pt-BR', {
compactDisplay: 'long',
currency: 'BRL',
style: 'currency'
})
balanceSpan.textContent = formater.format(balance)
}
E iremos completar essa parte criando uma função setup() que será responsável por cuidar do carregamento inicial da página, obtendo as transações, renderizando-as na tela e exibindo o saldo:
// ...
async function setup() {
const results = await fetchTransactions()
transactions.push(...results)
transactions.forEach(renderTransaction)
updateBalance()
}
document.addEventListener('DOMContentLoaded', setup)
Vamos agora passar para a criação de uma nova transação. Para isso vamos criar mais uma função, que ficará responsável por obter os valores do formulário, fazer a requisição POST e renderizar a transação devolvida na resposta:
Obs.: repare que ao fim da função também limpamos o formulário e atualizamos o saldo.
Obs².: repare também que precisamos criar um event listener para o form.
// ...
async function saveTransaction(ev) {
ev.preventDefault()
const name = document.querySelector('#name').value
const amount = parseFloat(document.querySelector('#amount').value)
const response = await fetch('<http://localhost:3000/transactions>', {
method: 'POST',
body: JSON.stringify({ name, amount }),
headers: {
'Content-Type': 'application/json'
}
})
const transaction = await response.json()
transactions.push(transaction)
renderTransaction(transaction)
ev.target.reset()
updateBalance()
}
// ...
document.addEventListener('DOMContentLoaded', setup)
document.querySelector('form').addEventListener('submit', saveTransaction)
Para realizar a edição de uma transação vamos ter um botão na lista de transações que carregará os dados dela para o formulário onde poderão ser editadas e enviadas. Vamos criar uma função para criar o elemento do botão e então renderizar um botão para cada transação na tela:
// ...
function createEditTransactionBtn(transaction) {
const editBtn = document.createElement('button')
editBtn.classList.add('edit-btn')
editBtn.textContent = 'Editar'
editBtn.addEventListener('click', () => {
document.querySelector('#id').value = transaction.id
document.querySelector('#name').value = transaction.name
document.querySelector('#amount').value = transaction.amount
})
return editBtn
}
// ...
Agora precisamos atualizar a função saveTransaction() para que faça uma requisição PUT se o input id estiver presente, ou seja, for uma transação já existente no backend, ou uma requisição POST se o input id não estiver presente:
// ...
async function saveTransaction(ev) {
ev.preventDefault()
const id = document.querySelector('#id').value
const name = document.querySelector('#name').value
const amount = parseFloat(document.querySelector('#amount').value)
if (id) {
const response = await fetch(`http://localhost:3000/transactions/${id}`, {
method: 'PUT',
body: JSON.stringify({ name, amount }),
headers: {
'Content-Type': 'application/json'
}
})
const transaction = await response.json()
const indexToRemove = transactions.findIndex((t) => t.id === id)
transactions.splice(indexToRemove, 1, transaction)
document.querySelector(`#transaction-${id}`).remove()
renderTransaction(transaction)
} else {
const response = await fetch('<http://localhost:3000/transactions>', {
method: 'POST',
body: JSON.stringify({ name, amount }),
headers: {
'Content-Type': 'application/json'
}
})
const transaction = await response.json()
transactions.push(transaction)
renderTransaction(transaction)
}
ev.target.reset()
updateBalance()
}
// ...
Por fim, só resta implementar a exclusão das transações. Para isso também criaremos um botão na tela para cada transação que ficará responsável pela exclusão da mesma. Depois de criado só precisamos renderizá-lo junto com as transações:
// ...
function createDeleteTransactionButton(id) {
const deleteBtn = document.createElement('button')
deleteBtn.classList.add('delete-btn')
deleteBtn.textContent = 'Excluir'
deleteBtn.addEventListener('click', async () => {
await fetch(`http://localhost:3000/transactions/${id}`, { method: 'DELETE' })
deleteBtn.parentElement.remove()
const indexToRemove = transactions.findIndex((t) => t.id === id)
transactions.splice(indexToRemove, 1)
updateBalance()
})
return deleteBtn
}
// ...
Agora que a aplicação está totalmente finalizada deixo para você um desafio extra, a estilização da página. Durante a criação dos elementos inserimos classes auxiliares que podem ajudar na estilização e você pode usá-las para criar uma página com o seu estilo, dando um toque pessoal ao nosso último exercício. Deixarei aqui um exemplo de estilização simples que eu fiz utilizando a exata estrutura que temos:
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 0;
}
body {
background-color: #1c1a1d;
color: #fff;
}
main {
display: flex;
gap: 2rem;
margin: 0 2rem;
}
main > form {
flex: 1;
}
main > div {
flex: 3;
}
h1 {
margin: 2rem 2rem 0;
}
h2 {
background-color: #39353b;
border-radius: .5rem;
margin-top: 2rem;
padding: 1rem 1.5rem;
}
label {
display: block;
margin: 1.5rem 0 .5rem;
}
input {
background-color: #39353b;
border: 0;
border-radius: .25rem;
color: #fff;
padding: .75rem;
width: 100%;
}
button {
background-color: #6bf394;
border: 0;
border-radius: .25rem;
font-size: .9rem;
padding: .5rem 1rem;
}
button:hover {
cursor: pointer;
filter: brightness(0.9);
}
form > button {
margin-top: 1rem;
width: 100%;
}
button + button {
margin-left: 1rem;
}
.transactions {
width: 100%;
}
.transaction {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #39353b;
margin-bottom: 1rem;
padding-bottom: 1rem;
}
.transaction:first-child {
margin-top: 1rem;
}
.transaction > span {
flex: 1;
}
.transaction-amount {
margin-right: 4rem;
text-align: end;
}
.transaction-amount.credit {
color: #6bf394;
}
.transaction-amount.debit {
color: #f64348;
}
.edit-btn {
background-color: #7bb4ff;
}
.delete-btn {
background-color: #f64348;
}
@media screen and (max-width: 768px) {
main {
flex-direction: column;
}
}