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:
Before we dive in, ensure you have the following:
You will learn how to use and implement these JavaScript libraries.
The following are the objectives of this tutorial:
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.
A finance tracker app is helpful because:
These are just a few practical uses of a finance tracker application.
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.
App overview:
First, let's set up the Strapi backend and implement the logic for managing budgets, income, and expenses.
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 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
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!
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:
Budgets
Create a new collection called Budget
.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.
Incomes
Follow the same steps you did to create the first collection above. Name this collection Incomes
. The fields you'll need for this collection are:Field Name | Data Type |
---|---|
description | Text - Short text |
amount | Number |
Expenses
Create another collection called Expenses
. The fields you'll need for this collection are:Field Name | Data Type |
---|---|
description | Text - Short text |
amount | Number |
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.
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.
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.
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.
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
Next, install the necessary JavaScript libraries or dependencies for this project:
npm install axios
tailwind.config.js
and postcss.config.js
.npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm i react-icons
Install using the command:
npm install date-fns
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".
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 the src
directory and a new file called SideNav.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
and IncomeForm.tsx
.
Next, Create an Overview.tsx
file inside the dashboard
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 the app
folder. This component is the main application component.
In this section, we get to the main task of integrating the data from Strapi into the frontend.
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.
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:
1import React from 'react'
2
3const Overview = () => {
4 return (
5 <>
6 <main>
7 <div>
8 <p>Overview</p>
9 </div>
10 </main>
11 </>
12 )
13}
14
15export 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.
Next up is the SideNav.tsx
component, where the page's routing will be located.
1'use client'
2import Link from "next/link";
3import { FaFileInvoice, FaWallet } from "react-icons/fa";
4import { MdDashboard } from "react-icons/md";
5import { usePathname } from 'next/navigation';
6
7const SideNav = () => {
8 const pathname = usePathname();
9
10 return (
11 <div>
12 <section>
13 <p>Tracker</p>
14 </section>
15
16 <section>
17 <Link href="/" >
18 <section>
19 <MdDashboard />
20 <span>Overview</span>
21 </section>
22 </Link>
23 </section>
24
25 <section>
26 <Link href="/dashboard/budget">
27 <section>
28 <FaFileInvoice />
29 <span>Budget</span>
30 </section>
31 </Link>
32 </section>
33
34 <section>
35 <Link href="/dashboard/cashflow">
36 <section>
37 <FaWallet />
38 <span>Cashflow</span>
39 </section>
40 </Link>
41 </section>
42 </div >
43 );
44};
45
46export default SideNav;
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:
1import SideNav from '@/components/SideNav'
2import Overview from './dashboard/Overview'
3
4const page = () => {
5 return (
6 <>
7 <div>
8 <SideNav />
9 <div>
10 <Overview />
11 </div>
12 </div>
13 </>
14 )
15}
16
17export 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.
Budget
PageThe 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:
1'use client'
2import React, { useEffect, useState } from 'react';
3import axios from 'axios';
Budget
TypeScript interface to define the structure of the budget
object:1interface Expense {
2 id: number;
3 attributes: {
4 category: string;
5 amount: number;
6 };
7}
1const [budgets, setBudgets] = useState<Budget[]>([]);
useEffect
hook to fetch budgets when the component mounts using the fetch()
API. The fetched data is stored in the budgets
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 the budget
state, else it logs an error.1useEffect(() => {
2 const fetchBudgets = () => {
3 fetch("http://localhost:1337/api/budgets?populate=budget")
4 .then((res) => {
5 if(!res.ok) {
6 throw new Error("Network response was not ok");
7 }
8 return res.json();
9 })
10 .then((data) => {
11 console.log("Fetched budgets:", data);
12 if (Array.isArray(data.data)) {
13 setBudgets(data.data);
14 } else {
15 console.error("Fetched data is not an array");
16 }
17 })
18 .catch((error) => {
19 console.error("Error fetching budgets:", error);
20 });
21 };
22
23 fetchBudgets();
24}, []);
map
method to map through the array and render each budget individually on the page, like this:1<section>
2 {budgets.length === 0 ? (
3 <>
4 <div>
5 <p>You haven't added a budget..</p>
6 </div>
7 </>
8 ) : (
9 <>
10 <article>
11 {budgets.map((budget) => (
12 <article key={budget.id}>
13 <section>
14 <p>{budget.attributes.category}</p>
15 <span>Budget planned</span>
16 <h1>${budget.attributes.amount}</h1>
17 </section>
18 </article>
19 ))}
20 </article >
21 </>
22 )}
23</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!
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.
Budget
component:1'use client';
2import React, { ChangeEvent, useEffect, useReducer, useState } from 'react';
3import axios from 'axios';
4import Budget from './Budget';
BudgetFormProps
interface by passing in three props. The first one, onClose
, will close the form. The second prop, setBudgets
will update the budgets
state in the parent component (Budget.tsx
component). The third one selectedBudget
will select a budget that's being edited or set it to null. 1interface BudgetFormProps {
2 onClose: () => void;
3 setBudgets: React.Dispatch<React.SetStateAction<Budget[]>>;
4 selectedBudget: Budget | null;
5}
1const BudgetForm: React.FC<BudgetFormProps> = ({
2 onClose,
3 setBudgets,
4 selectedBudget
5 }) => {
6 // Other functionalities
7}
initialState
object to set the initial states (values) for the form fields.1const initialState = {
2 category: 'food',
3 amount: 0,
4};
useReducer
hook to manage the form fields state, initializing it with the initialState
object.1function reducer(state = initialState, { field, value }: { field: string, value: any }) {
2 return { ...state, [field]: value };
3}
4
5const [formFields, dispatch] = useReducer(reducer, initialState);
useEffect
function that pre-fills the form when the user wants to edit budget data. It runs whenever the selectedBudget
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 when selectedBudget
changes.1useEffect(() => {
2 if (selectedBudget) {
3 for (const [key, value] of Object.entries(selectedBudget?.attributes)) {
4 dispatch({ field: key, value });
5 }
6 } else {
7 for (const [key, value] of Object.entries(initialState)) {
8 dispatch({ field: key, value });
9 }
10 }
11}, [selectedBudget]);
handleInputChange
function that will handle changes to the form input fields and update the corresponding state fields.
It will destructure name
and value
from the event target and then dispatch an action to update the state field corresponding to the name with the new value.1const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
2 const { name, value } = e.target;
3 ispatch({ field: name, value });
4};
handleSendBudget
. This function will handle the logic for sending a budget to the Strapi backend. It will send a POST
request to create a new budget or a PUT
request to update an existing one. It will first extract the necessary budget details from formFields
and then check if there is a selected budget.1const handleSendBudget = async () => {
2 try {
3 const { category, amount } = formFields;
4
5 if (selectedBudget) {
6 // Update an existing budget field
7 const data = await axios.put(`http://localhost:1337/api/budgets/${selectedBudget.id}`, {
8 data: { category, amount },
9 });
10 console.log(data)
11 setBudgets((prev) => prev.map((inv) => (inv.id === selectedBudget.id ? { ...inv, ...formFields } : inv)));
12 window.location.reload()
13 } else {
14 // Create a new budget
15 const { data } = await axios.post('http://localhost:1337/api/budgets', {
16 data: { category, amount },
17 });
18 console.log(data);
19 setBudgets((prev) => [...prev, data.data]);
20 }
21 onClose();
22 } catch (error) {
23 console.error(error);
24 }
25};
1<form>
2 <h2>{selectedBudget ? 'Edit Budget' : 'Create Budget'}</h2>
3 <button onClick={onClose}>
4 ×
5 </button>
6
7 <div>
8 <div>
9 <label htmlFor="category">
10 Budget category
11 </label>
12 <select
13 id="category"
14 name="category"
15 value={formFields.category}
16 onChange={handleInputChange}
17 >
18 <option value="food">Food</option>
19 <option value="transportation">Transportation</option>
20 <option value="housing">Housing</option>
21 <option value="savings">Savings</option>
22 <option value="miscellaneous">Miscellaneous</option>
23 </select>
24 </div>
25
26 <div>
27 <label htmlFor="amount">
28 Category Amount
29 </label>
30 <input
31 id="amount"
32 name="amount"
33 type="number"
34 placeholder="Input category amount"
35 onChange={handleInputChange}
36 value={formFields.amount}
37 required
38 />
39 </div>
40
41 <button
42 type="button"
43 onClick={handleSendBudget} >
44 {selectedBudget ? 'Update Budget' : 'Add Budget'}
45 </button>
46 </div>
47</form>
After styling our form as desired, here's how it might look like:
We have to implement the creation and edit functionalities in the parent component, Budget.tsx
.
1import BudgetForm from './BudgetForm';
2import { FaEdit, FaTrash } from 'react-icons/fa';
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.1const [isBudgetFormOpen, setIsBudgetFormOpen] = useState(false);
2const [selectedBudget, setSelectedBudget] = useState<Budget | null>(null);
handleOpenBudgetForm
and handleCloseBudgetForm
to handle the opening and closing of the form modal.1const handleOpenBudgetForm = () => {
2 setSelectedBudget(null);
3 setIsBudgetFormOpen(true);
4};
5
6const handleCloseBudgetForm = () => {
7 setSelectedBudget(null);
8 setIsBudgetFormOpen(false);
9};
1<button onClick={handleOpenBudgetForm}>
2 Add a budget
3</button>
handleEditBudget
function that will open the form and pre-populate the fields with the selected budget's data for editing. It will set the selectedBudget
to the budget that we want to edit. It will also open the form by setting isBudgetFormOpen
to true.1const handleEditBudget = (budget: Budget) => {
2 console.log("Editing:", budget);
3 setSelectedBudget(budget);
4 setIsBudgetFormOpen(true);
5};
onClick
event for the handleEditBudget
function in a button in the JSX:1<span>
2 <FaEdit onClick={() => handleEditBudget(budget)} />
3</span>
1{isBudgetFormOpen && (
2 <BudgetForm
3 onClose={handleCloseBudgetForm}
4 setBudgets={setBudgets}
5 selectedBudget={selectedBudget}
6 />
7)}
The last functionality for the CRUD operation is the delete functionality.
handleDeleteBudget
function that will delete a budget data by selecting its id
and sending a DELETE
request to the API. This ,will remove or filter out the deleted budget from the budget
state.1const handleDeleteBudget = async (id: number) => {
2 try {
3 alert("Are you sure you want to delete this budget?")
4 await axios.delete(`http://localhost:1337/api/budgets/${id}`);
5 setBudgets(budgets.filter((budget) => budget.id !== id));
6 } catch (error) {
7 console.error(error);
8 }
9};
onClick
event for the handleDeleteBudget
function in the trash icon in the JSX:1<span>
2 <FaTrash onClick={() => handleDeleteBudget(budget.id)} />
3</span>
We'll then render both the SideNav.tsx
component and the Budget.tsx
component on the main page. Go to the page.tsx
component located in the budget
folder and paste this:
1import SideNav from '@/components/SideNav'
2import Budget from './Budget'
3
4const page = () => {
5 return (
6 <>
7 <div>
8 <SideNav />
9
10 <div>
11 <Budget />
12 </div>
13 </div>
14 </>
15 )
16}
17export 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.
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.
Locate the ExpenseForm.tsx
component inside the expense
folder, and we'll paste these lines of code into it.
useState
and useEffect
to manage state and side effects and Axios to make HTTP requests.1import React, { useState, useEffect } from 'react';
2import axios from 'axios';
ExpenseFormProps.
This interface will specify the props the component expects. isOpen
determines if the form is open, the onClose
function closes the form, expense
is the expense to be edited or null, and refreshCashflow
is a function to refresh the cashflow column.1interface ExpenseFormProps {
2 isOpen: boolean;
3 onClose: () => void;
4 expense: { id: number, attributes: { description: string, amount: number } } | null;
5 refreshCashflow: () => void;
6}
ExpenseForm
component and initialize state variables description
and amount
for the two input fields we'll need.1const ExpenseForm: React.FC<ExpenseFormProps> = ({ isOpen, onClose, expense, refreshCashflow }) => {
2const [description, setDescription] = useState('');
3const [amount, setAmount] = useState<number>(0);
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.1useEffect(() => {
2 if (expense) {
3 console.log("Editing expense:", expense);
4 setDescription(expense.attributes.description);
5 setAmount(expense.attributes.amount);
6 } else {
7 setDescription('');
8 setAmount(0);
9 }
10}, [expense]);
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.1const handleSubmit = async (e: React.FormEvent) => {
2 e.preventDefault();
3
4 const expenseData = { description, amount };
5
6 try {
7 if (expense) {
8 await axios.put(`http://localhost:1337/api/expenses/${expense.id}`, { data: expenseData });
9 } else {
10 await axios.post('http://localhost:1337/api/expenses', { data: expenseData });
11 }
12 refreshCashflow();
13 onClose();
14 } catch (error) {
15 console.error('Error submitting expense:', error);
16 }
17};
null
to prevent rendering.1if (!isOpen) return null;
Let's go ahead and render the modal form UI. We'll add input fields for the description
and the amount
with their corresponding labels. The form submission will be handled by handleSubmit
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.
1<form onSubmit={handleSubmit}>
2 <h2>
3 {expense ? 'Edit Expense' : 'Add Expense'}
4 </h2>
5 <button
6 onClick={onClose}>
7 ×
8 </button>
9
10 <div>
11 <div >
12 <label htmlFor="description">
13 Description
14 </label>
15 <input
16 id="description"
17 name="description"
18 type="text"
19 placeholder="Input description"
20 value={description}
21 onChange={(e) => setDescription(e.target.value)}
22 required
23 />
24 </div>
25
26 <div>
27 <label htmlFor="amount">
28 Category Amount
29 </label>
30 <input
31 id="amount"
32 name="amount"
33 type="number"
34 placeholder="Input amount"
35 value={amount}
36 onChange={(e) => setAmount(parseFloat(e.target.value))}
37 required
38 />
39 </div>
40
41 <button type="submit">
42 {expense ? 'Edit Expense' : 'Add Expense'}
43 </button>
44 </div>
45</form>
That's what it takes to create the form logic for creating and editing an expense.
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:
1'use client'
2import React, { useEffect, useState } from 'react';
3import axios from 'axios';
4import ExpenseForm from './ExpenseForm';
5import { format, parseISO } from 'date-fns';
6import { FaEdit, FaPlus, FaTrash } from 'react-icons/fa';
7import { BsThreeDotsVertical } from "react-icons/bs";
8
9interface Expense {
10 id: number;
11 attributes: {
12 description: string;
13 createdAt: string;
14 amount: number;
15 };
16}
17
18interface ExpenseProps {
19 refreshCashflow: () => void;
20}
21
22const Expense: React.FC<ExpenseProps> = ({ refreshCashflow }) => {
23 const [expenses, setExpenses] = useState<Expense[]>([]);
24 const [isExpenseFormOpen, setIsExpenseFormOpen] = useState(false);
25 const [selectedExpense, setSelectedExpense] = useState<Expense | null>(null);
26 const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
27
28 useEffect(() => {
29 fetchExpenses();
30 }, []);
31
32 const fetchExpenses = () => {
33 fetch("http://localhost:1337/api/expenses?populate=expense")
34 .then((res) => {
35 if (!res.ok) {
36 throw new Error("Network response was not ok");
37 }
38 return res.json();
39 })
40 .then((data) => {
41 if (Array.isArray(data.data)) {
42 setExpenses(data.data);
43 } else {
44 console.error("Fetched data is not an array");
45 }
46 })
47 .catch((error) => {
48 console.error("Error fetching expenses:", error);
49 });
50 };
51
52 const handleOpenExpenseForm = () => {
53 setSelectedExpense(null);
54 setIsExpenseFormOpen(true);
55 };
56
57 const handleCloseExpenseForm = () => {
58 setSelectedExpense(null);
59 setIsExpenseFormOpen(false);
60 };
61
62 const handleEditExpense = (expense: Expense) => {
63 setSelectedExpense(expense);
64 setIsExpenseFormOpen(true);
65 };
66
67 const handleDeleteExpense = async (id: number) => {
68 try {
69 await axios.delete(`http://localhost:1337/api/expenses/${id}`);
70 setExpenses(expenses.filter((expense) => expense.id !== id));
71 refreshCashflow();
72 } catch (error) {
73 console.error(error);
74 }
75 };
76
77 const formatDate = (dateString: string) => {
78 const date = parseISO(dateString);
79 return format(date, 'yyyy-MM-dd HH:mm:ss');
80 };
81
82 const toggleDropdown = (id: number) => {
83 setDropdownOpen(dropdownOpen === id ? null : id);
84 };
85
86 return (
87 <section>
88 <div>
89 <h1>Expenses</h1>
90 <FaPlus />
91 </div>
92 <div>
93 {expenses.map((expense) => (
94 <div key={expense.id}>
95 <div>
96 <p>{expense.attributes.description}</p>
97 <div>
98 <BsThreeDotsVertical onClick={() => toggleDropdown(expense.id)} />
99
100 {dropdownOpen === expense.id && (
101 <div>
102 <FaEdit onClick={() => handleEditExpense(expense)} /> Edit
103 <FaTrash onClick={() => handleDeleteExpense(expense.id)} /> Delete
104 </div>
105 )}
106 </div>
107 </div>
108 <div>
109 <span>{formatDate(expense.attributes.createdAt)}</span>
110 <h1>${expense.attributes.amount}</h1>
111 </div>
112 </div>
113 ))}
114 </div>
115 {isExpenseFormOpen && (
116 <ExpenseForm
117 isOpen={isExpenseFormOpen}
118 onClose={() => {
119 handleCloseExpenseForm();
120 fetchExpenses();
121 }}
122 expense={selectedExpense}
123 refreshCashflow={() => {
124 refreshCashflow();
125 fetchExpenses();
126 }}
127 />
128 )}
129 </section>
130 );
131};
132
133export 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 interface ExpenseProps
to specify the props the component expects. In this case, it is refreshCashflow
, which is a function to refresh cashflow.
Next, we declare the Expense component and initialize state variables expenses
, isExpenseFormOpen
, selectedExpense
, and dropdownOpen
.
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 the expenses
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 the format
and parseISO
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. When isExpenseFormOpen
is true, it renders the ExpenseForm
component with the appropriate props. The form will be conditionally rendered based on the isOpen
prop.
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.
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.
IncomeForm.tsx
component inside the income
folder, we'll paste the same code in our ExpenseForm.tsx
component. We'll then change components or texts from expense
to income
and Expense
to Income
, even the imported component, etc.Implementing the functionalities for the 'Income' page:
In the Income.tsx
component inside the income
folder, we'll paste the same code that's in our Expense.tsx
We'll follow the same steps to implement the income functionalities. Don't forget to change components or texts from expense
to income
and Expense
to Income
, even your import.
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:
1'use client'
2import React, { useEffect, useState } from 'react';
3import axios from 'axios';
4import Income from './income/Income';
5import Expense from './expense/Expense';
6import { format, parseISO } from 'date-fns';
7
8interface CashflowItem {
9 id: number;
10 type: 'income' | 'expense';
11 description: string;
12 createdAt: string;
13 amount: number;
14}
15
16const Cashflow: React.FC = () => {
17 const [cashflow, setCashflow] = useState<CashflowItem[]>([]);
18
19 const fetchCashflow = async () => {
20 try {
21 const incomesResponse = await axios.get('http://localhost:1337/api/incomes?populate=income');
22 const expensesResponse = await axios.get('http://localhost:1337/api/expenses?populate=expense');
23
24 const incomes = incomesResponse.data.data.map((income: any) => ({
25 id: income.id,
26 type: 'income',
27 description: income.attributes.description,
28 createdAt: income.attributes.createdAt,
29 amount: income.attributes.amount,
30 }));
31
32 const expenses = expensesResponse.data.data.map((expense: any) => ({
33 id: expense.id,
34 type: 'expense',
35 description: expense.attributes.description,
36 createdAt: expense.attributes.createdAt,
37 amount: expense.attributes.amount,
38 }));
39
40 // Combine incomes and expenses and sort by createdAt in descending order
41 const combined = [...incomes, ...expenses].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
42 setCashflow(combined);
43 } catch (error) {
44 console.error('Error fetching cashflow:', error);
45 }
46 };
47
48 useEffect(() => {
49 fetchCashflow();
50 }, []);
51
52 return (
53 <main>
54 <Income refreshCashflow={fetchCashflow} />
55
56 <section>
57 <div>
58 <h1>Cashflow</h1>
59 </div>
60 <div>
61 {cashflow.map((item) => (
62 <div key={item.id}>
63 <div>
64 <p>{item.description}</p>
65 <span>{format(parseISO(item.createdAt), 'yyyy-MM-dd HH:mm:ss')}</span>
66 <h1>
67 ${item.amount.toFixed(2)}
68 </h1>
69 </div>
70 </div>
71 ))}
72 </div>
73 </section>
74
75 <Expense refreshCashflow={fetchCashflow} />
76 </main>
77 );
78};
79
80export 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 variable cashflow
. This state will hold an array of cash flow items.
Next, we created afetchCashflow
function to fetch incomes and expenses data from the API. We used the axios.get
method to send GET requests to the respective API endpoints.
Maping 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. This createdAt
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 the fetchCashflow
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
and Expense
components, passing fetchCashflow
as the refreshCashflow
prop. This allows it to trigger a cashflow refresh whenever an action is performed in either of the respective components.
page.tsx
component located in that same cashflow
folder.1import SideNav from '@/components/SideNav'
2import Cashflow from './Cashflow'
3
4const page = () => {
5 return (
6 <>
7 <div>
8 <SideNav />
9
10 <div>
11 <Cashflow />
12 </div>
13 </div>
14 </>
15 )
16}
17export 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.
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
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 a fetchBudgetLimit
function to fetch the budget limit data:
1const fetchBudgetLimit = async () => {
2 try {
3 const res = await axios.get("http://localhost:1337/api/budget-limits");
4 if (res.data.data && res.data.data[0]) {
5 setBudgetLimit(res.data.data[0].attributes.limit);
6 }
7 } catch (error) {
8 console.error("Error fetching budget limit:", error);
9 }
10};
useEffect
hook:
fetchBudgetLimit();
1const totalBudgetedAmount = budgets.reduce((total, budget) => total + budget.attributes.amount, 0);
1<section>
2 <h3>Budget Limit: ${budgetLimit}</h3>
3 <h3>Total Budgeted: ${totalBudgetedAmount}</h3>
4</section>
1{isBudgetFormOpen && (
2 <BudgetForm
3 onClose={handleCloseBudgetForm}
4 setBudgets={setBudgets}
5 selectedBudget={selectedBudget}
6 budgetLimit={budgetLimit}
7 totalBudgetedAmount={totalBudgetedAmount}
8 />
9)}
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.
BudgetFormProps
to look like this:1interface BudgetFormProps {
2 onClose: () => void;
3 setBudgets: React.Dispatch<React.SetStateAction<Budget[]>>;
4 selectedBudget: Budget | null;
5 budgetLimit: number | null;
6 totalBudgetedAmount: number;
7}
1const BudgetForm: React.FC<BudgetFormProps> = ({ onClose, setBudgets, selectedBudget, budgetLimit, totalBudgetedAmount }) => {
2 //other lines of code
3}
const [error, setError] = useState<string | null>(null);
handleSendBudget
function to include the new functionality:1const handleSendBudget = async () => {
2 try {
3 const { category, amount } = formFields;
4 const newAmount = parseFloat(amount);
5 const currentAmount = selectedBudget ? selectedBudget.attributes.amount : 0;
6 const newTotal = totalBudgetedAmount - currentAmount + newAmount;
7
8 if (budgetLimit !== null && newTotal > budgetLimit) {
9 setError("You've exceeded the budget limit for this month");
10 return;
11 }
12
13 if (selectedBudget) {
14 // Update an existing budget
15 const data = await axios.put(`http://localhost:1337/api/budgets/${selectedBudget.id}`, {
16 data: { category, amount: newAmount },
17 });
18 console.log(data);
19 setBudgets((prev) => prev.map((inv) => (inv.id === selectedBudget.id ? { ...inv, ...formFields, amount: newAmount } : inv)));
20 window.location.reload();
21 } else {
22 // Create a new budget
23 const { data } = await axios.post('http://localhost:1337/api/budgets', {
24 data: { category, amount: newAmount },
25 });
26 console.log(data);
27 setBudgets((prev) => [...prev, data.data]);
28 }
29
30 setError(null);
31 onClose();
32 } catch (error) {
33 console.error(error);
34 setError("An error occurred while saving the budget.");
35 }
36 };
{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.
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.
Juliet is a developer and technical writer whose work aims to break down complex technical concepts and empower developers of all levels.