Build a Finance Tracker with Next.js, Strapi, and Chartjs: Part 1

Juliet Ofoegbu - Aug 2 - - Dev Community

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:

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.

App overview:
Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Image description

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:

  1. Collection 1 - Budgets Create a new collection called Budget.

Image description

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

Image description

Click the 'Finish' button, and your Budget collection type should now look like this:

Image description

Ensure to click the 'Save' button and wait for the app to restart the server.

  1. Collection 2 - 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
  1. Collection 3 - 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

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.

Image description

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.

Image description

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:

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install Dependencies

Next, install the necessary JavaScript libraries or dependencies for this project:

  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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 and postcss.config.js.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode
  1. React Icons: A package to include icons in your project.
npm i react-icons
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode

Start the Next.js App

We'll start up the frontend app with the following command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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.tsxpage 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This is how the page layout should look like after styling:

Image description

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 the budget 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 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.

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();
}, []);
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

After styling this page, this is what it looks like now:

Image description

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 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.

interface BudgetFormProps {
   onClose: () => void;
   setBudgets: React.Dispatch<React.SetStateAction<Budget[]>>;
   selectedBudget: Budget | null;
}
Enter fullscreen mode Exit fullscreen mode
  • 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,
};
Enter fullscreen mode Exit fullscreen mode
  • 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 the initialState object.
function reducer(state = initialState, { field, value }: { field: string, value: any }) {
   return { ...state, [field]: value };
}

const [formFields, dispatch] = useReducer(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode
  • We'll define the 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.
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]);
Enter fullscreen mode Exit fullscreen mode
  • We'll then define a 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.
const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
    const { name, value } = e.target;
    ispatch({ field: name, value });
};
Enter fullscreen mode Exit fullscreen mode
  • We'll create another function -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.
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);
     }
};
Enter fullscreen mode Exit fullscreen mode
  • Let's now render the form UI.
<form>
    <h2>{selectedBudget ? 'Edit Budget' : 'Create Budget'}</h2>
    <button onClick={onClose}>
        &times;
    </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>
Enter fullscreen mode Exit fullscreen mode

After styling our form as desired, here's how it might look like:

Image description

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';
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode
  • We'll then create two functions - handleOpenBudgetForm and handleCloseBudgetForm to handle the opening and closing of the form modal.
const handleOpenBudgetForm = () => {
    setSelectedBudget(null);
    setIsBudgetFormOpen(true);
};

const handleCloseBudgetForm = () => {
    setSelectedBudget(null);
    setIsBudgetFormOpen(false);
};
Enter fullscreen mode Exit fullscreen mode
  • Let's add a button to open the form modal:
<button onClick={handleOpenBudgetForm}>
    Add a budget
</button>
Enter fullscreen mode Exit fullscreen mode
  • 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 the selectedBudget to the budget that we want to edit. It will also open the form by setting isBudgetFormOpen to true.
const handleEditBudget = (budget: Budget) => {
    console.log("Editing:", budget);
    setSelectedBudget(budget);
    setIsBudgetFormOpen(true);
};
Enter fullscreen mode Exit fullscreen mode
  • Let's go ahead and add an onClick event for the handleEditBudget 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}
    />
)}
Enter fullscreen mode Exit fullscreen mode

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 its id and sending a DELETE request to the API. This ,will remove or filter out the deleted budget from the budget 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);
    }
};
Enter fullscreen mode Exit fullscreen mode
  • Now, we'll add an onClick event for the handleDeleteBudget 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 the Budget.tsx component on the main page. Go to the page.tsx component located in the budget 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
Enter fullscreen mode Exit fullscreen mode

When we go to our browser, we'll see the changes made.

This is how it should look after styling:

Image description

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 and useEffect 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, 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.

    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 variables description and amount 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 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.

<form onSubmit={handleSubmit}>
    <h2>
        {expense ? 'Edit Expense' : 'Add Expense'}
    </h2>
    <button
        onClick={onClose}>
        &times;
    </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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

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.

  1. Building the income form modal:
    In the 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.

  2. 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

  3. 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.

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:

Image description

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;
Enter fullscreen mode Exit fullscreen mode

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 income and expenses data from the API. We used the axios.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. 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.

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 same cashflow folder.
import SideNav from '@/components/SideNav'
import Cashflow from './Cashflow'

const page = () => {
    return (
        <>
          <div>
             <SideNav />

             <div>
                 <Cashflow />
             </div>
          </div>
        </>
    )
}
export default page
Enter fullscreen mode Exit fullscreen mode

This is our cash flow page, which now consists of both the income and expense functionalities after styling.

Image description

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 a fetchBudgetLimit 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.");
    }
  };
Enter fullscreen mode Exit fullscreen mode
  • 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.

Image description

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .