Introduction
This is part one of this blog series, where we'll learn to set up the Strapi backend with collections, create the app's user interface, connect it to Strapi CMS, and implement the functionalities for budget, income, and expenses to build a finance tracker application.
For reference, here's the outline of this blog series:
- Part 1: Setting up Strapi & implementing the app's CRUD functionalities.
- Part 2: Adding visualization with charts.
- Part 3: Setting up user authentication and personalized reporting.
Prerequisites
Before we dive in, ensure you have the following:
- NodeJs installed on your local machine.
- Basic understanding of Next.js and CRUD operations. You can check out the Epic Next.js 14 Tutorial Part 1: Learn Next.js by building a real-life project.
- Experience with RESTful APIs. You can check out Strapi's REST APIs docs.
- Ensure you have a good knowledge of Tailwind CSS. You will be using it to style the front-end of this page based on your preference.
Technologies
You will learn how to use and implement these JavaScript libraries.
- Chart.js for building charts visualization of the financial data
- html-to-pdfmake for generating PDF from HTML content.
- pdfmake for generating PDFs for both client-side and server-side
- html2canvas to take screenshots of the webpage directly on the browser.
Tutorial Objectives
The following are the objectives of this tutorial:
- Learn about the full-stack development process.
- Set up backend with Strapi.
- Master frontend development using Next.js for interface.
- Visualizing financial data.
- Setting up user authentication.
- Adding a personalized financial report feature that users can export.
What We Are Building
In this tutorial, we will build a Finance Tracker app. This app is designed to help individuals or organizations monitor and manage their financial activities. It lets users record and track budgets, income, expenses, and other financial transactions.
Why is a Finance Tracker Application Useful?
A finance tracker app is helpful because:
- It helps users become more aware of their spending habits and financial status.
- It helps users set and stick to budgets.
- It helps users set financial goals, e.g., car savings.
These are just a few practical uses of a finance tracker application.
Project Overview
At the end of this tutorial, we can create budgets, manage income and expenses through the app, visualize their data, and use advanced features like authentication and personalized financial reports.
First, let's set up the Strapi backend and implement the logic for managing budgets, income, and expenses.
Setting Up Strapi Backend
In this section, we'll set up Strapi as the backend platform for storing financial data (budget, income, and expenses). Strapi will aslo serve as our RESTful API.
Create Project Directory
Create a directory for your project. This directory will contain the project's Strapi backend folder and the Next.js frontend folder.
mkdir finance-tracker && cd finance-tracker
Create a Strapi Project
Next, you have to create a new Strapi project, and you can do this using this command:
npx create-strapi-app@latest backend --quickstart
This will create a new Strapi application in the finance-tracker
directory and install necessary dependencies, such as Strapi plugins.
After a successful installation, your default browser automatically opens a new tab for the Strapi admin panel at "http://localhost:1337/admin." Suppose it doesn't just copy the link provided in the terminal and paste it into your browser.
Fill in your details on the form provided and click on the "Let's start" button.
Your Strapi admin panel is ready for use!
Create Collection Type and Content Types
If you click on the 'Content-Type Builder' tab at the left sidebar, you'll see that there's already a collection type named 'User'. Create a new collection type by clicking on the "+ Create new collection type". After that, proceed to create the following collections:
- Collection 1 -
Budgets
Create a new collection calledBudget
.
These are the fields we'll need for this collection. So go ahead and create them:
Field Name | Data Type |
---|---|
category |
Enumeration |
amount |
Number |
In the category
field above, which is an Enumeration
type, provide the following as shown in the image below:
food
transportation
housing
savings
Click the 'Finish' button, and your Budget
collection type should now look like this:
Ensure to click the 'Save' button and wait for the app to restart the server.
-
Collection 2 -
Incomes
Follow the same steps you did to create the first collection above. Name this collectionIncomes
. The fields you'll need for this collection are:
Field Name | Data Type |
---|---|
description |
Text - Short text |
amount |
Number |
-
Collection 3 -
Expenses
Create another collection calledExpenses
. The fields you'll need for this collection are:
Field Name | Data Type |
---|---|
description |
Text - Short text |
amount |
Number |
Create Entries
Fill in the amount field and choose an option for the category
field. Save it and then hit the 'Publish' button to add the entry.
NOTE: They are saved as drafts by default, so you need to publish it to view it.
Do the same for the other two collections. Create entries for each of their respective fields, save, and publish.
Set API Permissions
The last step of setting up our Strapi backend is to grant users permission to create, find, edit, and delete budgets in the app. To do this, go to the Settings > Roles > USERS & PERMISSIONS PLUGIN section. Select Public.
Toggle the 'Budgets' section and then check the 'Select all' checkbox. By checking this box, we will allow users to perform CRUD operations. Save it.
Toggle the 'Incomes' and 'Expenses' sections and check the 'Select all' checkbox to grant users access to perform CRUD operations on these collections.
Don't forget to save it.
That is all for the Strapi backend configuration. Let's move on to the frontend.
Creating the User Interface with Next.js
Using Next.js, we'll build the frontend view that allows users to view and manage their budget, income, and expenses. It'll also handle the logic and functionalities.
Testing the API Endpoints from Strapi
To ensure that the RESTful API endpoints from Strapi are all working well, paste the endpoints into your browser so you can see the entries you created.
For the first collection Budgets
, paste the URL "http://localhost:1337/api/budgets" in your browser.
You'll get this:
We will do the same for the income endpoint "http://localhost:1337/api/incomes", and the expenses endpoint at "http://localhost:1337/api/expenses" to see their respective entries.
If we can view the entries, it means our endpoints are ready to be used.
Create a New Next.js Application
Go to your root directory /finance-tracker
. Create your Next.js project using this command:
npx create-next-app frontend
When prompted in the command line, choose to use 'Typescript' for the project. Select 'Yes' for the ESLint, 'Yes' to use the src/
directory, and 'Yes' for the experimental app
directory to complete the set-up. This selection will create a new Next.js app.
Navigate to the app you just created using the command below:
cd frontend
Install Dependencies
Next, install the necessary JavaScript libraries or dependencies for this project:
- Axios: Axios is a JavaScript library used to send asynchronous HTTP requests to REST endpoints. It's commonly used to perform CRUD operations. Install Axios using this command:
npm install axios
-
Tailwind CSS: We'll be using this CSS framework to style our application.
Install Tailwind CSS and then run the init command to generate both
tailwind.config.js
andpostcss.config.js
.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
- React Icons: A package to include icons in your project.
npm i react-icons
- date-fns: This is a JavaScript date library to ensure consistent date and time formatting in an application. We'll be using it to format the date entries are made.
Install using the command:
npm install date-fns
Start the Next.js App
We'll start up the frontend app with the following command:
npm run dev
View it on your browser using the link "http://localhost:3000".
Folder Structure
Here's an overview of the complete folder structure:
src/
┣ app/
┃ ┣ dashboard/
┃ ┃ ┣ budget/
┃ ┃ ┃ ┣ Budget.tsx
┃ ┃ ┃ ┣ BudgetForm.tsx
┃ ┃ ┃ ┗ page.tsx
┃ ┃ ┣ cashflow/
┃ ┃ ┃ ┣ expense/
┃ ┃ ┃ ┃ ┣ Expense.tsx
┃ ┃ ┃ ┃ ┗ ExpenseForm.tsx
┃ ┃ ┃ ┣ income/
┃ ┃ ┃ ┃ ┣ Income.tsx
┃ ┃ ┃ ┃ ┗ IncomeForm.tsx
┃ ┃ ┃ ┣ Cashflow.tsx
┃ ┃ ┃ ┗ page.tsx
┃ ┃ ┗ Overview.tsx
┃ ┣ globals.css
┃ ┣ head.tsx
┃ ┣ layout.tsx
┃ ┗ page.tsx
┣ components/
┃ ┗ SideNav.tsx
Our app will have a dashboard interface where users can view and manage their budget, income, and expenses. Let's list out the folders and components we'll be needing for the layout:
We'll work mainly with two folders, some sub-folders, and some files for this project. So, we'll create some folders inside the
app
directory.We'll create a
components
folder inside thesrc
directory and a new file calledSideNav.tsx
inside it.In the app directory, we'll create a folder called
dashboard
with two subfolders.
The first subfolder, budget,
will contain three component files named page.tsx
, BudgetForm.tsx
, and Budget.tsx
. This is the folder where the routing for the budget page will be located.
The second subfolder, cashflow,
will also contain two folders and two component files: page.tsx
and Cashflow.tsx
. This is the folder where the routing for the cash-flow (income & expenses) page will be located.
The two folders inside this cashflow
folder should be named expense
and income
for easy identification. The expense
folder will contain two components: Expense.tsx
and ExpenseForm.tsx
.
The same is true for the
income
folder. It should have two components:Income.tsx
andIncomeForm.tsx
.Next, Create an
Overview.tsx
file inside thedashboard
folder. This component will be the dashboard page route. It will be used sparingly in this part, but it'll come in handy later on.The last component file we'll work with is the
page.tsx
component, located directly inside theapp
folder. This component is the main application component.
Integrating with Strapi
In this section, we get to the main task of integrating the data from Strapi into the frontend.
Fetching Data from Strapi API and Displaying Budget Info
Let's fetch the data entries from the Strapi collections we created and display them on the frontend page.
We'll start with the 'Budget' information, but first, let's create the page layout.
Layout of the Overall Application
The Overview.tsx
page will be the first one as it's the first route. It won't be useful now, but we'll write the JSX for it like this:
import React from 'react'
const Overview = () => {
return (
<>
<main>
<div>
<p>Overview</p>
</div>
</main>
</>
)
}
export default Overview
Note: I'll be omitting the styling so that it won't take up too much space here. It will be available in the GitHub repo I'll provide at the end of this article.
Building the Side Navigation View
Next up is the SideNav.tsx
component, where the page's routing will be located.
'use client'
import Link from "next/link";
import { FaFileInvoice, FaWallet } from "react-icons/fa";
import { MdDashboard } from "react-icons/md";
import { usePathname } from 'next/navigation';
const SideNav = () => {
const pathname = usePathname();
return (
<div>
<section>
<p>Tracker</p>
</section>
<section>
<Link href="/" >
<section>
<MdDashboard />
<span>Overview</span>
</section>
</Link>
</section>
<section>
<Link href="/dashboard/budget">
<section>
<FaFileInvoice />
<span>Budget</span>
</section>
</Link>
</section>
<section>
<Link href="/dashboard/cashflow">
<section>
<FaWallet />
<span>Cashflow</span>
</section>
</Link>
</section>
</div >
);
};
export default SideNav;
Rendering the Layout Components
For the final layout of the main app, we'll render the two components we worked on recently. Locate the pages.tsx
component inside the app
directory and paste these lines of code:
import SideNav from '@/components/SideNav'
import Overview from './dashboard/Overview'
const page = () => {
return (
<>
<div>
<SideNav />
<div>
<Overview />
</div>
</div>
</>
)
}
export default page
This is how the page layout should look like after styling:
When we click the "Budget" tab, we'll be directed to the budget page. When we click the "Cashflow" tab, we'll be directed to the cashflow (income & expenses) page.
Building the Budget
Page
The budget
folder is where all components related to the budget page will be located.
In the Budget.tsx
file of our budget
folder located in dashboard
directory, build the budget page layout:
-
Import the library and hooks we'll be using:
'use client' import React, { useEffect, useState } from 'react'; import axios from 'axios';
-
Set the
Budget
TypeScript interface to define the structure of thebudget
object:interface Expense { id: number; attributes: { category: string; amount: number; }; }
-
Set the state as an array to store the budgets we'll fetch from the Strapi backend:
const [budgets, setBudgets] = useState<Budget[]>([]);
Use the
useEffect
hook to fetch budgets when the component mounts using thefetch()
API. The fetched data is stored in thebudgets
state we set above.
An error will be thrown if the data response fails. If it's successful and passed as JSON, the function then checks to see if it's an array.
If it is, then we'll be able to map through it to render and display the budget data. It then saves this data in thebudget
state, else it logs an error.
useEffect(() => {
const fetchBudgets = () => {
fetch("http://localhost:1337/api/budgets?populate=budget")
.then((res) => {
if(!res.ok) {
throw new Error("Network response was not ok");
}
return res.json();
})
.then((data) => {
console.log("Fetched budgets:", data);
if (Array.isArray(data.data)) {
setBudgets(data.data);
} else {
console.error("Fetched data is not an array");
}
})
.catch((error) => {
console.error("Error fetching budgets:", error);
});
};
fetchBudgets();
}, []);
- We'll use the
map
method to map through the array and render each budget individually on the page, like this:
<section>
{budgets.length === 0 ? (
<>
<div>
<p>You haven't added a budget..</p>
</div>
</>
) : (
<>
<article>
{budgets.map((budget) => (
<article key={budget.id}>
<section>
<p>{budget.attributes.category}</p>
<span>Budget planned</span>
<h1>${budget.attributes.amount}</h1>
</section>
</article>
))}
</article >
</>
)}
</section>
After styling this page, this is what it looks like now:
We've successfully fetched the budget data from the Strapi backend and displayed it on the frontend page. Great!
Building the Modal for the Budget Form
This will be an interactive application, meaning users will be able to interact with the app and add new budgets to the endpoint. To enable the creation of new budgets, we want a modal to open up when we perform an action (click a button).
Inside this modal, there will be a form where we must fill in the details necessary to create a new budget. The field names will be the same as the ones we created (category and amount) when we set up the collection for the budgets in Strapi.
We also want to be able to edit any budget data. The form for editing a budget will be the same as the one for creating a new budget, so let's make the form modal component.
Open the BudgetForm.tsx
component in the budget
folder.
-
First, we'll import the necessary libraries, hooks, and the
Budget
component:'use client'; import React, { ChangeEvent, useEffect, useReducer, useState } from 'react'; import axios from 'axios'; import Budget from './Budget';
Next, we'll define the
BudgetFormProps
interface by passing in three props. The first one,onClose
, will close the form. The second prop,setBudgets
will update thebudgets
state in the parent component (Budget.tsx
component). The third oneselectedBudget
will select a budget that's being edited or set it to null.
interface BudgetFormProps {
onClose: () => void;
setBudgets: React.Dispatch<React.SetStateAction<Budget[]>>;
selectedBudget: Budget | null;
}
-
We'll pass these props into this component so that they can be used by other components:
const BudgetForm: React.FC<BudgetFormProps> = ({ onClose, setBudgets, selectedBudget }) => { // Other functionalities }
Then, we'll define the
initialState
object to set the initial states (values) for the form fields.
const initialState = {
category: 'food',
amount: 0,
};
- Next is the reducer function that will update the state based on the field and value provided to handle form field updates. Then we'll use the
useReducer
hook to manage the form fields state, initializing it with theinitialState
object.
function reducer(state = initialState, { field, value }: { field: string, value: any }) {
return { ...state, [field]: value };
}
const [formFields, dispatch] = useReducer(reducer, initialState);
- We'll define the
useEffect
function that pre-fills the form when the user wants to edit budget data. It runs whenever theselectedBudget
changes. It will populate the form fields with the budget data if there is a selected budget. If there is no selected budget, the form fields are reset to their initial values. The[selectedBudget]
dependency array ensures this effect runs only whenselectedBudget
changes.
useEffect(() => {
if (selectedBudget) {
for (const [key, value] of Object.entries(selectedBudget?.attributes)) {
dispatch({ field: key, value });
}
} else {
for (const [key, value] of Object.entries(initialState)) {
dispatch({ field: key, value });
}
}
}, [selectedBudget]);
- We'll then define a
handleInputChange
function that will handle changes to the form input fields and update the corresponding state fields. It will destructurename
andvalue
from the event target and then dispatch an action to update the state field corresponding to the name with the new value.
const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
ispatch({ field: name, value });
};
- We'll create another function -
handleSendBudget
. This function will handle the logic for sending a budget to the Strapi backend. It will send aPOST
request to create a new budget or aPUT
request to update an existing one. It will first extract the necessary budget details fromformFields
and then check if there is a selected budget.
const handleSendBudget = async () => {
try {
const { category, amount } = formFields;
if (selectedBudget) {
// Update an existing budget field
const data = await axios.put(`http://localhost:1337/api/budgets/${selectedBudget.id}`, {
data: { category, amount },
});
console.log(data)
setBudgets((prev) => prev.map((inv) => (inv.id === selectedBudget.id ? { ...inv, ...formFields } : inv)));
window.location.reload()
} else {
// Create a new budget
const { data } = await axios.post('http://localhost:1337/api/budgets', {
data: { category, amount },
});
console.log(data);
setBudgets((prev) => [...prev, data.data]);
}
onClose();
} catch (error) {
console.error(error);
}
};
- Let's now render the form UI.
<form>
<h2>{selectedBudget ? 'Edit Budget' : 'Create Budget'}</h2>
<button onClick={onClose}>
×
</button>
<div>
<div>
<label htmlFor="category">
Budget category
</label>
<select
id="category"
name="category"
value={formFields.category}
onChange={handleInputChange}
>
<option value="food">Food</option>
<option value="transportation">Transportation</option>
<option value="housing">Housing</option>
<option value="savings">Savings</option>
<option value="miscellaneous">Miscellaneous</option>
</select>
</div>
<div>
<label htmlFor="amount">
Category Amount
</label>
<input
id="amount"
name="amount"
type="number"
placeholder="Input category amount"
onChange={handleInputChange}
value={formFields.amount}
required
/>
</div>
<button
type="button"
onClick={handleSendBudget} >
{selectedBudget ? 'Update Budget' : 'Add Budget'}
</button>
</div>
</form>
After styling our form as desired, here's how it might look like:
Implement Create/Edit Functionalities in The 'Budget' Component
We have to implement the creation and edit functionalities in the parent component, Budget.tsx
.
- Let's first import the form component and some icons from react-icons.
import BudgetForm from './BudgetForm';
import { FaEdit, FaTrash } from 'react-icons/fa';
- We'll create two states. The first one
isBudgetFormOpen
, will store the state of the budget form component - if it's opened or closed(default). The second one,selectedBudget
will store the state of any selected budget.
const [isBudgetFormOpen, setIsBudgetFormOpen] = useState(false);
const [selectedBudget, setSelectedBudget] = useState<Budget | null>(null);
- We'll then create two functions -
handleOpenBudgetForm
andhandleCloseBudgetForm
to handle the opening and closing of the form modal.
const handleOpenBudgetForm = () => {
setSelectedBudget(null);
setIsBudgetFormOpen(true);
};
const handleCloseBudgetForm = () => {
setSelectedBudget(null);
setIsBudgetFormOpen(false);
};
- Let's add a button to open the form modal:
<button onClick={handleOpenBudgetForm}>
Add a budget
</button>
- Now, let's define the
handleEditBudget
function that will open the form and pre-populate the fields with the selected budget's data for editing. It will set theselectedBudget
to the budget that we want to edit. It will also open the form by settingisBudgetFormOpen
to true.
const handleEditBudget = (budget: Budget) => {
console.log("Editing:", budget);
setSelectedBudget(budget);
setIsBudgetFormOpen(true);
};
-
Let's go ahead and add an
onClick
event for thehandleEditBudget
function in a button in the JSX:<span> <FaEdit onClick={() => handleEditBudget(budget)} /> </span>
Conditionally, we'll render the form component at the bottom of the 'Budget' component:
{isBudgetFormOpen && (
<BudgetForm
onClose={handleCloseBudgetForm}
setBudgets={setBudgets}
selectedBudget={selectedBudget}
/>
)}
Implement the Delete Functionality
The last functionality for the CRUD operation is the delete functionality.
- Let's create a
handleDeleteBudget
function that will delete a budget data by selecting itsid
and sending aDELETE
request to the API. This ,will remove or filter out the deleted budget from thebudget
state.
const handleDeleteBudget = async (id: number) => {
try {
alert("Are you sure you want to delete this budget?")
await axios.delete(`http://localhost:1337/api/budgets/${id}`);
setBudgets(budgets.filter((budget) => budget.id !== id));
} catch (error) {
console.error(error);
}
};
-
Now, we'll add an
onClick
event for thehandleDeleteBudget
function in the trash icon in the JSX:<span> <FaTrash onClick={() => handleDeleteBudget(budget.id)} /> </span>
We'll then render both the
SideNav.tsx
component and theBudget.tsx
component on the main page. Go to thepage.tsx
component located in thebudget
folder and paste this:
import SideNav from '@/components/SideNav'
import Budget from './Budget'
const page = () => {
return (
<>
<div>
<SideNav />
<div>
<Budget />
</div>
</div>
</>
)
}
export default page
When we go to our browser, we'll see the changes made.
This is how it should look after styling:
This is functional now. The 'create', 'edit', and 'delete' functionalities should work as expected.
Fetching Data from Strapi API to Display the Expenses
The cash flow page code will be similar to the code you used to create the budget page. It will be a single page, 'Cashflow', but will render the income
and expenses
pages.
Remember the cashflow
folder we created at the beginning of this tutorial, that has 2 sub-folders (expense
and income
)?, we'll open it up.
Building the Expenses Form Modal
Locate the ExpenseForm.tsx
component inside the expense
folder, and we'll paste these lines of code into it.
-
We'll first import the necessary libraries:
useState
anduseEffect
to manage state and side effects and Axios to make HTTP requests.import React, { useState, useEffect } from 'react'; import axios from 'axios';
-
We'll then define the TypeScript interface
ExpenseFormProps.
This interface will specify the props the component expects.isOpen
determines if the form is open, theonClose
function closes the form,expense
is the expense to be edited or null, andrefreshCashflow
is a function to refresh the cashflow column.interface ExpenseFormProps { isOpen: boolean; onClose: () => void; expense: { id: number, attributes: { description: string, amount: number } } | null; refreshCashflow: () => void; }
-
We'll declare the
ExpenseForm
component and initialize state variablesdescription
andamount
for the two input fields we'll need.const ExpenseForm: React.FC<ExpenseFormProps> = ({ isOpen, onClose, expense, refreshCashflow }) => { const [description, setDescription] = useState(''); const [amount, setAmount] = useState<number>(0);
-
We'll then implement the
useEffect
hook to update the form fields when expenses change. If expense is not null, it will populate the fields with its attributes. If it is null, it will reset the fields.useEffect(() => { if (expense) { console.log("Editing expense:", expense); setDescription(expense.attributes.description); setAmount(expense.attributes.amount); } else { setDescription(''); setAmount(0); } }, [expense]);
-
Next, we'll define a
handleSubmit
function to handle the form submission. If an expense exists, we'll update it using the PUT request; otherwise, we'll create a new expense using the POST request. Refresh the cash flow page after the change and close the form after the request.const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const expenseData = { description, amount }; try { if (expense) { await axios.put(`http://localhost:1337/api/expenses/${expense.id}`, { data: expenseData }); } else { await axios.post('http://localhost:1337/api/expenses', { data: expenseData }); } refreshCashflow(); onClose(); } catch (error) { console.error('Error submitting expense:', error); } };
-
If the form is not open, it will return
null
to prevent rendering.if (!isOpen) return null;
-
Let's go ahead and render the modal form UI. We'll add input fields for the
description
and theamount
with their corresponding labels. The form submission will be handled byhandleSubmit
function.As part of the modal form rendering, we will add a submit button with conditional text based on whether an expense is being edited or created.
<form onSubmit={handleSubmit}>
<h2>
{expense ? 'Edit Expense' : 'Add Expense'}
</h2>
<button
onClick={onClose}>
×
</button>
<div>
<div >
<label htmlFor="description">
Description
</label>
<input
id="description"
name="description"
type="text"
placeholder="Input description"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="amount">
Category Amount
</label>
<input
id="amount"
name="amount"
type="number"
placeholder="Input amount"
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value))}
required
/>
</div>
<button type="submit">
{expense ? 'Edit Expense' : 'Add Expense'}
</button>
</div>
</form>
That's what it takes to create the form logic for creating and editing an expense.
Implementing the Functionalities for the 'Expense' Page
Now, to implement the functionalities inside the main expense page, let's locate the Expense.tsx
component inside the expense
folder.
Paste these lines of code into it:
'use client'
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import ExpenseForm from './ExpenseForm';
import { format, parseISO } from 'date-fns';
import { FaEdit, FaPlus, FaTrash } from 'react-icons/fa';
import { BsThreeDotsVertical } from "react-icons/bs";
interface Expense {
id: number;
attributes: {
description: string;
createdAt: string;
amount: number;
};
}
interface ExpenseProps {
refreshCashflow: () => void;
}
const Expense: React.FC<ExpenseProps> = ({ refreshCashflow }) => {
const [expenses, setExpenses] = useState<Expense[]>([]);
const [isExpenseFormOpen, setIsExpenseFormOpen] = useState(false);
const [selectedExpense, setSelectedExpense] = useState<Expense | null>(null);
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
useEffect(() => {
fetchExpenses();
}, []);
const fetchExpenses = () => {
fetch("http://localhost:1337/api/expenses?populate=expense")
.then((res) => {
if (!res.ok) {
throw new Error("Network response was not ok");
}
return res.json();
})
.then((data) => {
if (Array.isArray(data.data)) {
setExpenses(data.data);
} else {
console.error("Fetched data is not an array");
}
})
.catch((error) => {
console.error("Error fetching expenses:", error);
});
};
const handleOpenExpenseForm = () => {
setSelectedExpense(null);
setIsExpenseFormOpen(true);
};
const handleCloseExpenseForm = () => {
setSelectedExpense(null);
setIsExpenseFormOpen(false);
};
const handleEditExpense = (expense: Expense) => {
setSelectedExpense(expense);
setIsExpenseFormOpen(true);
};
const handleDeleteExpense = async (id: number) => {
try {
await axios.delete(`http://localhost:1337/api/expenses/${id}`);
setExpenses(expenses.filter((expense) => expense.id !== id));
refreshCashflow();
} catch (error) {
console.error(error);
}
};
const formatDate = (dateString: string) => {
const date = parseISO(dateString);
return format(date, 'yyyy-MM-dd HH:mm:ss');
};
const toggleDropdown = (id: number) => {
setDropdownOpen(dropdownOpen === id ? null : id);
};
return (
<section>
<div>
<h1>Expenses</h1>
<FaPlus />
</div>
<div>
{expenses.map((expense) => (
<div key={expense.id}>
<div>
<p>{expense.attributes.description}</p>
<div>
<BsThreeDotsVertical onClick={() => toggleDropdown(expense.id)} />
{dropdownOpen === expense.id && (
<div>
<FaEdit onClick={() => handleEditExpense(expense)} /> Edit
<FaTrash onClick={() => handleDeleteExpense(expense.id)} /> Delete
</div>
)}
</div>
</div>
<div>
<span>{formatDate(expense.attributes.createdAt)}</span>
<h1>${expense.attributes.amount}</h1>
</div>
</div>
))}
</div>
{isExpenseFormOpen && (
<ExpenseForm
isOpen={isExpenseFormOpen}
onClose={() => {
handleCloseExpenseForm();
fetchExpenses();
}}
expense={selectedExpense}
refreshCashflow={() => {
refreshCashflow();
fetchExpenses();
}}
/>
)}
</section>
);
};
export default Expense;
This is similar to the code in the Budget.tsx
component.
Code Explanation:
As usual, we'll first import the necessary libraries and components -
ExpenseForm.
We'll use the JavaScript data library
date-fns
for consistent date and time formatting to avoid time zone issues arising from using JavaScript's Date object. We've already installed it at the beginning of the tutorial.Then, we'll define the TypeScript interface
Expense
to represent the structure of an expense object and the TypeScript interfaceExpenseProps
to specify the props the component expects. In this case, it isrefreshCashflow
, which is a function to refresh cashflow.Next, we declare the Expense component and initialize state variables
expenses
,isExpenseFormOpen
,selectedExpense
, anddropdownOpen
.We'll use the
useEffect
hook to fetch expenses when the component mounts.Next, we'll define the
fetchExpenses
function to fetch expenses from the API. If the response is not OK, it will throw an error. If data is an array, it will update theexpenses
state. Otherwise, it will log an error.
We'll now define some handler functions:
-
handleOpenExpenseForm
to open the form for adding a new expense. -
handleCloseExpenseForm
to close the form. -
handleEditExpense
to open the form to edit a selected expense. -
handleDeleteExpense
to delete an expense from the API and update the state.- Next, we'll create
formatDate
function to format date and time consistently using theformat
andparseISO
property from the date-fns library. - The last function is the
toggle dropdown
function to handle the opening and closing of dropdown menus for each expense. This dropdown menu will contain the texts 'Edit' and 'Delete' to enable the two functionalities on the page. - Finally, we'll render the
Expense
component, which will display a list of expenses. Each expense item has options to edit or delete. WhenisExpenseFormOpen
is true, it renders theExpenseForm
component with the appropriate props. The form will be conditionally rendered based on theisOpen
prop.
- Next, we'll create
NB: The
createdAt
attribute is gotten from the Strapi backend. It gives you the exact date and time an item was created.
Let's test our application to ensure it works as expected.
Fetching Data from Strapi API to Display the Income
Since the income functionality is similar to the expenses functionality, we'll follow the steps we used to create the expense section to create this income section.
Building the income form modal:
In theIncomeForm.tsx
component inside theincome
folder, we'll paste the same code in ourExpenseForm.tsx
component. We'll then change components or texts fromexpense
toincome
andExpense
toIncome
, even the imported component, etc.Implementing the functionalities for the 'Income' page:
In theIncome.tsx
component inside theincome
folder, we'll paste the same code that's in ourExpense.tsx
We'll follow the same steps to implement the income functionalities. Don't forget to change components or texts from
expense
toincome
andExpense
toIncome
, even your import.
Combining the 'Income' and 'Expense' Component in a Single Page
We'll want expenses and income to be on the same page, not separate pages. So, what we'll do is to combine the two components and display them on the 'cashflow' page like this:
We want to fill out the blank 'Cashflow' column. One other functionality we'll include is that we add incomes and expenses; it should also show up in the 'Cashflow' column arranged in order of time created.
It should function the same way transaction history of a mobile bank app does, where you have a list of all debits and credits all in one page arranged according to the date the transactions were made.
In the Cashflow.tsx
component, we'll add these lines of code:
'use client'
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import Income from './income/Income';
import Expense from './expense/Expense';
import { format, parseISO } from 'date-fns';
interface CashflowItem {
id: number;
type: 'income' | 'expense';
description: string;
createdAt: string;
amount: number;
}
const Cashflow: React.FC = () => {
const [cashflow, setCashflow] = useState<CashflowItem[]>([]);
const fetchCashflow = async () => {
try {
const incomesResponse = await axios.get('http://localhost:1337/api/incomes?populate=income');
const expensesResponse = await axios.get('http://localhost:1337/api/expenses?populate=expense');
const incomes = incomesResponse.data.data.map((income: any) => ({
id: income.id,
type: 'income',
description: income.attributes.description,
createdAt: income.attributes.createdAt,
amount: income.attributes.amount,
}));
const expenses = expensesResponse.data.data.map((expense: any) => ({
id: expense.id,
type: 'expense',
description: expense.attributes.description,
createdAt: expense.attributes.createdAt,
amount: expense.attributes.amount,
}));
// Combine incomes and expenses and sort by createdAt in descending order
const combined = [...incomes, ...expenses].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
setCashflow(combined);
} catch (error) {
console.error('Error fetching cashflow:', error);
}
};
useEffect(() => {
fetchCashflow();
}, []);
return (
<main>
<Income refreshCashflow={fetchCashflow} />
<section>
<div>
<h1>Cashflow</h1>
</div>
<div>
{cashflow.map((item) => (
<div key={item.id}>
<div>
<p>{item.description}</p>
<span>{format(parseISO(item.createdAt), 'yyyy-MM-dd HH:mm:ss')}</span>
<h1>
${item.amount.toFixed(2)}
</h1>
</div>
</div>
))}
</div>
</section>
<Expense refreshCashflow={fetchCashflow} />
</main>
);
};
export default Cashflow;
Code explanation:
We imported the necessary libraries and components and set the Typescript interface
CashflowItem
to define the structure of the cashflow items.We then declared the
Cashflow
component and initialized a state variablecashflow
. This state will hold an array of cash flow items.Next, we created a
fetchCashflow
function to fetch income and expenses data from the API. We used theaxios.get
method to send GET requests to the respective API endpoints.Mapping the responses to format them into arrays of objects similar to the
CashflowItem
interface comes next.Then we combined the incomes and expenses into a single array and sort them by
createdAt
date in descending order. ThiscreatedAt
property was gotten from Strapi.Next, we updated the
cashflow
state with the combined and sorted data.Then, we implemented the
useEffect
hook to call thefetchCashflow
function when the component mounts. The empty dependency array[]
ensures this effect runs only once.Finally, we rendered the UI of the cashflow column, mapping through the cashflow state to render each cashflow item. Include the
Income
andExpense
components, passingfetchCashflow
as therefreshCashflow
prop. This allows it to trigger a cashflow refresh whenever an action is performed in either of the respective components.
Combine Income and Expenses in the Cashflow Page
- Now render it along with the side navigation like this inside the
page.tsx
component located in that samecashflow
folder.
import SideNav from '@/components/SideNav'
import Cashflow from './Cashflow'
const page = () => {
return (
<>
<div>
<SideNav />
<div>
<Cashflow />
</div>
</div>
</>
)
}
export default page
This is our cash flow page, which now consists of both the income and expense functionalities after styling.
We're almost done with our app. Now, let's add one last functionality for this part.
Setting Budget Limit
We want to be able to set a budget limit amount that users won't be able to exceed when creating budgets.
The total of all the budget amounts must not be more than the budget limit amount set. If the user attempts to set an amount greater than the limit, they will get an error message saying "You've exceeded the budget limit for this month".
Create the Budget Limit Collection in Strapi
- In the Strapi admin panel, go to Content-Types Builder.
- Add a new collection type and name it "BudgetLimits".
- Add a field named limit of type 'Number'.
- Save the changes.
- Create an entry, save, and publish.
- We got to roles settings and updated the permission to enable CRUD functionalities.
Fetch the Budget Limit and Update the Budget Component
Let's go to our Budget.tsx
file.
We'll create a state for the budget limit:
const [budgetLimit, setBudgetLimit] = useState<number | null>(null);
-
Inside our
useEffect
function, we'll create afetchBudgetLimit
function to fetch the budget limit data:const fetchBudgetLimit = async () => { try { const res = await axios.get("http://localhost:1337/api/budget-limits"); if (res.data.data && res.data.data[0]) { setBudgetLimit(res.data.data[0].attributes.limit); } } catch (error) { console.error("Error fetching budget limit:", error); } };
Next, we'll call the budget limit function in the
useEffect
hook:
fetchBudgetLimit();
-
Let's now create a function that will calculate the total amount of budget categories inputted:
const totalBudgetedAmount = budgets.reduce((total, budget) => total + budget.attributes.amount, 0);
-
Next, we'll update the rendered UI to include the budget limit amount and the total amount of budget categories inputted:
<section> <h3>Budget Limit: ${budgetLimit}</h3> <h3>Total Budgeted: ${totalBudgetedAmount}</h3> </section>
-
Lastly, update the rendered budget form component to include the props that will be passed from the form:
{isBudgetFormOpen && ( <BudgetForm onClose={handleCloseBudgetForm} setBudgets={setBudgets} selectedBudget={selectedBudget} budgetLimit={budgetLimit} totalBudgetedAmount={totalBudgetedAmount} /> )}
We'll see the total budget amount and the limit value displayed in our app.
Update the Budget Form Component
We'll need to update the BudgetForm
to check the total budgeted amount against the limit before submitting.
-
We'll add props for the budget limit and total budget amount by updating our
BudgetFormProps
to look like this:interface BudgetFormProps { onClose: () => void; setBudgets: React.Dispatch<React.SetStateAction<Budget[]>>; selectedBudget: Budget | null; budgetLimit: number | null; totalBudgetedAmount: number; }
-
We'll then pass the recently added props into the component like this:
const BudgetForm: React.FC<BudgetFormProps> = ({ onClose, setBudgets, selectedBudget, budgetLimit, totalBudgetedAmount }) => { //other lines of code }
We'll create a state to store an error message when a user exceeds the budget limit set:
const [error, setError] = useState<string | null>(null);
Let's update the
handleSendBudget
function to include the new functionality:
const handleSendBudget = async () => {
try {
const { category, amount } = formFields;
const newAmount = parseFloat(amount);
const currentAmount = selectedBudget ? selectedBudget.attributes.amount : 0;
const newTotal = totalBudgetedAmount - currentAmount + newAmount;
if (budgetLimit !== null && newTotal > budgetLimit) {
setError("You've exceeded the budget limit for this month");
return;
}
if (selectedBudget) {
// Update an existing budget
const data = await axios.put(`http://localhost:1337/api/budgets/${selectedBudget.id}`, {
data: { category, amount: newAmount },
});
console.log(data);
setBudgets((prev) => prev.map((inv) => (inv.id === selectedBudget.id ? { ...inv, ...formFields, amount: newAmount } : inv)));
window.location.reload();
} else {
// Create a new budget
const { data } = await axios.post('http://localhost:1337/api/budgets', {
data: { category, amount: newAmount },
});
console.log(data);
setBudgets((prev) => [...prev, data.data]);
}
setError(null);
onClose();
} catch (error) {
console.error(error);
setError("An error occurred while saving the budget.");
}
};
- Lastly, we'll render the error message in the form:
{error && <p className="text-red-500 text-sm">{error}</p>}
Our app will be updated now. It will display the budget limit and the total budget amount at the top of our budget page. It will then implement the error message when the user attempts to exceed the set budget limit.
To test this out, we'll input a budget limit amount on our Strapi CMS admin panel, save it, and publish it. When we go back to the app, we'll try to update an amount in any budget category that will exceed the budget limit set. We'll get an error message letting us know that we can't perform that action because we are attempting to exceed our set budget amount limit.
We're done with part one of this blog series. Stay tuned for part two, where we'll continue this tutorial by adding visualization for our financial data using Chart.js.
Conclusion
In this part one of this tutorial series, we learned how to set up Strapi for backend storage, how to create collections and entries, and how to connect it to the frontend.
You also learned how to build a functional, interactive personal finance app where a user can create and track budgets, income and expenses. This should enrich your frontend development skills.
In the next part, we will learn how to add visualization with charts and graphs.
Top comments (0)