Build an Income tracker with NextJS using Cloudinary and Xata

Odewole Babatunde Samson - Nov 29 '22 - - Dev Community

An income tracker allows you to keep track of the inflow of cash available each month - making it especially helpful to stay on budget. At its most basic level, tracking your income and expenses on a regular basis helps you stay up-to-date with your financial information.

In this article, you'll build an app that tracks your income and saves and delete your data on the database. You'll be exploring the rich capabilities of Xata, a serverless database, and Cloudinary, a cloud-based image and video management service.

Prerequisites

The following is required to follow along smoothly with the tutorial:

Creating a Next.js application

To create the Next.js app, go to your terminal or command line. Using the cd command, we navigate to the directory we want our app to be created.

cd <directory-name>
Enter fullscreen mode Exit fullscreen mode

Once inside the directory, we create a new project using the command:

npx create-next-app

# or

yarn create next-app
Enter fullscreen mode Exit fullscreen mode

Once that's finished, we navigate into that directory and start a hot-reloading development server for the project on http://localhost:3000 with:

npm run dev
#or
yarn dev
Enter fullscreen mode Exit fullscreen mode

Installing Cloudinary

Cloudinary provides a robust solution to store, transform, optimize and deliver images and videos in software applications.

We’ll install the cloudinary-react package that exposes various media delivery and transformation functions using the command line.

npm i cloudinary-react lodash
Enter fullscreen mode Exit fullscreen mode

Lodash is a dependency of the Cloudinary package.

Setting up the Xata database

Create a new database on your Xata dashboard called income-tracker

Xata-database

Click on the created database and add a table titled incomes. Next, add a desc column of type String, price column of type Integer, and a date column of type Date to the table.

Your database should look like this:

xata-database-schema

Setting up the Xata instance

To set up Xata, you'll need to install the CLI globally;

npm install @xata.io/cli -g
Enter fullscreen mode Exit fullscreen mode

Then, authorize Xata to log you in;

xata auth login
Enter fullscreen mode Exit fullscreen mode

Next, select Create a new API key in browser from the prompts in your terminal. This opens your browser, where you can type in any name you choose. Once successful, you will get a display page indicating that you are all set.

Now cd into the Next.js project created earlier and run xata init to initialize Xata in your project;

cd <directory-name>
xata init
Enter fullscreen mode Exit fullscreen mode

The command above initializes the project with some questions in the terminal. This is where you select the name of the database created earlier, then select Generate Typescript code and choose your output source as util/xata.ts which is where the Xata codegen will be generated.

Creating the income tracker interface

You'll need to create a folder named components in your Nextjs project, which will include the Header, IncomeForm, IncomeList and IncomeItem components.

Now, inside the Header.tsx file and paste the below code:

import Image from 'next/image';
const Header = ({ totalIncome } : any) => {
  return (
    <header>
      <h1 className="text-3xl font-bold text-[#888]">Income Tracker</h1>
      <Image
        className=" object-cover rounded-xl"
        width={80}
        height={1}
        src="https://res.cloudinary.com/beswift/image/upload/v1647849821/samples/cloudinary-icon.png"
        alt="plant"
      />
      <div className="total-income">${totalIncome}</div>
    </header>
  );
}
export default Header
Enter fullscreen mode Exit fullscreen mode

From the code above, the totalIncome props were passed to the header and also the Cloudinary logo.

Next, inside the IncomeForm.tsx, paste the below code:

import React, { useRef, useState } from "react";
const IncomeForm = ({ income, setIncome }: any) => {
  const [values, setValues] = useState({
    desc: "",
    price: "",
    date: "",
  });
  const desc = useRef<HTMLInputElement | null>(null);
  const date = useRef(null);
  const price = useRef<HTMLInputElement | null>(null);
  const handleChange = (event: any) => {
    setValues({
      ...values,
      [event.target.name]: event.target.value,
    });
  };
  const AddIncome = (e: { preventDefault: () => void }) => {
    e.preventDefault();
    const { date, price, desc } = values;
    let d = date.split("-").map((e) => parseInt(e));
    let newD = new Date(d[0], d[1] - 1, d[2]);
    const obj = {
      desc: desc,
      price: price,
      date: newD.toISOString(),
    };
    fetch("/api/add-income-item", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(obj),
    })
      .then((res) => res.json())
      .then(() => {
        setIncome([...income, obj]);
        setValues({
          desc: "",
          price: "",
          date: "",
        });
      })
      .catch(() => alert("An error occured"));
  };
  return (
    <form className="income-form" onSubmit={AddIncome}>
      <div className="form-inner ">
        <input
          type="text"
          name="desc"
          id="desc"
          placeholder="Income description..."
          value={values.desc}
          onChange={handleChange}
        />
        <input
          className="text-xl"
          type="number"
          name="price"
          id="price"
          placeholder="Price"
          value={values.price}
          onChange={handleChange}
        />
        <input
          type="date"
          name="date"
          id="date"
          placeholder="Income date"
          value={values.date}
          onChange={handleChange}
        />
        <input className="bg-[#FFCE00]" type="submit" value="Add Income" />
      </div>
    </form>
  );
};
export default IncomeForm;
Enter fullscreen mode Exit fullscreen mode

The code snippet above did the following:

  • Import required dependencies and image collections
  • Create state variables to manage selected desc, price, and date of the income
  • A handleChange function to control form inputs
  • A AddIncome function that passed the income desc, price, and date to the xata database
  • Rendered the income form element to the web page

Next, the IncomeItem.tsx will display the list of the incomes on the screen

import React from "react";
function IncomeItem({ income, index, removeIncome } : any) {
  let date = new Date(income.date);
  let day = date.getDate();
  let month = date.getMonth() + 1;
  let year = date.getFullYear();
  const removeHandle = (i: any) => {
      fetch("/api/delete-income", {
        method: "DELETE",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({id: income.id}),
      })
        .then((res) => res.json())
        .then(() => {
    removeIncome(i);
        })
        .catch(() => alert("An error occured"));
  };
  return (
    <div className="income-item">
      <button className="remove-item" onClick={() => removeHandle(index)}>
        X
      </button>
      <div className="desc">{income.desc}</div>
      <div className="price">${income.price}</div>
      <div className="date">{day + "/" + month + "/" + year}</div>
    </div>
  );
}
export default IncomeItem;
Enter fullscreen mode Exit fullscreen mode

The code snippet above did the following:

  • Import required dependencies
  • A removeHandle function to delete data from the database

Next, create the IncomeList.tsx that list all the income like this:

import React from "react";
import IncomeItem from "./IncomeItem";
function IncomeList({ income, setIncome } : any) {
  const removeIncome = (i: any) => {
    let temp = income.filter((v: any, index: number) => index != i);
    setIncome(temp);
  };
  const sortByDate = (a: { date: number; }, b: { date: number; }) => {
    return a.date - b.date;
  }
  return (
    <div className="income-list">
      {income.sort(sortByDate).map((value: any, index: any) => (
        <IncomeItem
          key={index}
          income={value}
          index={index}
          removeIncome={removeIncome}
        />
      ))}
    </div>
  );
}
export default IncomeList;
Enter fullscreen mode Exit fullscreen mode

The code snippet above did the following:

  • Import required dependencies
  • A removeIncome function to remove incomes according to the index
  • Loop through the incomes and display

Storing data in the database

Create a new file in the api folder and name it add-income-item.ts. Paste the code below in the file:

import type { NextApiRequest, NextApiResponse } from 'next'
import { getXataClient } from "../../util/xata";
const xata = getXataClient();
export type Response = {
  success: boolean;
  message?: string;
};
const handler = async (
  req: NextApiRequest,
  res: NextApiResponse<Response>
) => {
  try {
  const { desc, price, date } = req.body;
  await xata.db.incomes.create({
    desc,
    price: parseFloat(price),
    date,
  });
  res.json({
    success: true,
  });
  } catch (error: any) {
    console.log(error)
  res.json({
    success: false,
    message: error?.message
  });
  }
};
export default handler;
Enter fullscreen mode Exit fullscreen mode

You get the data sent to the API and save it to the Xata database.

Deleting data in the database

Create another new file in the api folder and name it delete-income.ts. Paste the code below in the file:

import type { NextApiRequest, NextApiResponse } from "next";
import { getXataClient } from "../../util/xata";
const xata = getXataClient();
export type Response = {
  success: boolean;
  message?: string;
};
const handler = async (req: NextApiRequest, res: NextApiResponse<Response>) => {
  try {
    const { id } = req.body;
    await xata.db.incomes.delete({
      id: id
    });
    res.json({
      success: true,
    });
  } catch (error: any) {
    console.log(error);
    res.json({
      success: false,
      message: error?.message,
    });
  }
};
export default handler;
Enter fullscreen mode Exit fullscreen mode

You get to delete data from the Xata database through the API.

To see the app in action, run npm run dev, and visit the URL. You should see a screen like this.

income-tracker-application

income-tracker xata-database

Using Jamstack technologies, you have successfully built an income tracker application. You can view the project demo here and access the source code in this GitHub repo.

Conclusion

In this article, you created an income tracker app that helped you track and manage your income and expenses. You also explore using Xata for seamless database storage and Cloudinary for easy image uploads.

Resources

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