Introduction
In this tutorial, you'll learn how to build an invoicing app using Next.js and Strapi as the backend and content management. This tutorial is a detailed guide on how to create and set up a Strapi project, how to create Strapi collections, and how to connect the Strapi backend to the Next.js frontend to build a functional invoicing app with CRUD functionalities.
An invoice is a document issued by a seller or service provider to a buyer or client requesting payment for goods bought or services rendered. The invoice includes information such as the items purchased or services provided, payment information, amount of goods, agreed-upon rate, total price of good/services, shipping address, and so on.
An invoicing app is an application or software that allows you to create/generate invoices that can be downloaded in any format and emailed to a client. It is usually designed as a template so that users do not have to create invoice layouts from scratch, and it includes all transaction details between a buyer/client and a seller.
Prerequisites
- Ensure to have NodeJs installed on your local machine.
- Basic understanding of Next.js.
- Understanding of CRUD operations.
- Experience with RESTful APIs.
Project Overview
The invoicing app we'll be working on will allow users to generate or add invoices that will be sent to the Strapi backend, as well as fetch, update, and delete invoices.
These features will be created with Next.js for the frontend UI and logic, Strapi CMS for invoice storage, and the jsPDF library to make invoices downloadable.
Here's an image of what we're going to build:
Setting up Strapi Backend
In this section, we'll set up Strapi as the backend platform to store the invoice data.
Create Project Directory
Create a directory for your project. This directory will contain the Strapi backend folder and the Next.js frontend folder for the project.
mkdir invoicing-app && cd invoicing-app
Create a Strapi Project
Next is 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 `invoicing-app` directory and install necessary dependencies like Strapi plugins.
After a successful installation, your default browser automatically opens up a new tab for the Strapi admin panel at "http://localhost:1337/admin". If it doesn't, just copy the link provided in the terminal and paste in your browser. Fill in your details on the form provided and click on the "Let's start" button.
Your Strapi admin dashboard is ready for use!
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/82sp5jwrm6k78yf4cz60.png)
### 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**".
A modal box with a form will open up. For the 'Display name' field, enter 'Invoices'. The API ID (Singular) and API ID (Plular) will be automatically generated.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vlb5aptukg2i2uwkma04.png)
When you click the "continue" button, you will be taken to the next page where you'll have to select the fields for your content type. For this project, these are the fields you'll need:
| Field Name | Data Type |
|------------------|-------------------|
| `name` | Text - Short text |
| `senderEmail` | Email |
| `recipientEmail` | Email |
| `date` | Date |
| `dueDate` | Date |
| `shippingAddress` | Text - Long text |
| `invoiceNote` | Text - Long text |
| `description` | Text - Short text |
| `qty` | Number |
| `rate` | Number |
| `total` | Number |
Click the 'Finish' button and your `invoice` collection type should now look like this:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jdtmixjv9hh8oikpcdlu.png)
Save this action by clicking on the '**Save**' button located at the top right-hand corner of the screen. This will restart the server so wait for it to reload.
### Create Entries
To test this, you can add an entry for this collection. Click on the **'Content Manager'** at the sidebar and then go to **'Invoices'**. Click the "**+ Create new entry**" button at the top right corner of the screen.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2zlx1wbv4kj2jvi4r5l5.png)
Fill in some details in the fields. 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._
### Enable Public API Access
The last set up here is to grant permission for user to create, find, edit, and delete invoices in the app. To do this, go to **Settings** on the side panel, click on **Roles** under the **USERS & PERMISSIONS PLUGIN** section. Select **Public**.
Toggle the **'Invoices'** section and then check the **'Select all'** checkbox. This will allow access to all CRUD operations. Save it.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0x1ot4098g97u8m7x9jv.png)
## Setting up the Next.js project
Here, we'll set up the Next.js project to build the frontend view that will allow users to view fetched invoices, create invoices, edit invoices, and delete invoices.
### Create a Next.js App
Go to your root directory `/invoicing-app`. Create your Next.js project using this command:
```bash
npx create-next-app frontend
```
When prompted in the command line, choose to use **'Typescript'** for the project. Select **'Yes'** for the ESLint, and **'Yes'** to use the `src/` directory, **'Yes'** for the experimental `app` directory to complete the set up. This will create a new Next.js app.
Navigate to the app you just created using the command below:
```bash
cd frontend
```
### Install Dependencies
Next install the necessary dependencies for this project :
1. [**Axios:**](https://www.npmjs.com/package/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:
```bash
npm install axios
```
2. [**jsPDF**](https://www.npmjs.com/package/jspdf): We want users to be able to download any invoice they create in a PDF format. Now instead of using the regular window print method, let's use this JavaScript jsPDF library which is customizable. It's a library used for generating PDFs in JavaScript. With jsPDF, you can format and customize the layout of your generated PDF.
3. [jspdf-autotable](https://www.npmjs.com/package/jspdf-autotable): We'll use jsPDF along with the [jspdf-autotable](https://www.npmjs.com/package/jspdf-autotable), a jsPDF plugin for generating tables. This jsPDF plugin adds the ability to generate PDF tables either by parsing HTML tables or by using Javascript data directly.
Install these libraries using this command:
```bash
npm i jspdf jspdf-autotable
```
Start up your frontend app with the following command:
```bash
npm run dev
```
Access it on your browswer using the link "http://localhost:3000".
### Project folder structure
- For this app, we'll need 3 files namely (`pages.tsx`, `Invoices.tsx`, and `InvoiceForm.tsx`) to make this app work. If you want to style your app with regular CSS, you can make changes to your CSS files. In this article, you will use TailwindCSS to style your application.
- Create a new folder inside the `src` folder called `components`. Create 2 components or files inside this folder and name them: `Invoices.tsx` and `InvoiceForm.tsx`.
The `Invoices` component will be where all the invoices created will be displayed. It will also be the main page of the application. The `InvoiceForm` component is for the form modal where users will have to input details to create or edit an invoice.
- In the `app` directory, locate `pages.tsx` and replace the code with these lines of code:
```javascript
'use client'
import Invoices from "../components/Invoices";
function App() {
return (
<div className="p-5">
<Invoices />
</div>
);
}
export default App;
```
The main component which is the `Invoices.tsx` component is imported and rendered as the main page of the application.
### Building the components and adding CRUD functionalities
Here, we'll build the app's components and add the CRUD functionalities to enable users fetch invoices from the Strapi backend, create new invoices, edit invoices, and delete invoices.
#### Create the Invoice Form
In your `InvoiceForm.tsx` component, paste these lines of code:
```js
"use client";
import React, { ChangeEvent, useEffect, useReducer, useState } from "react";
import axios from "axios";
interface InvoiceFormProps {
onClose: () => void;
setInvoices: React.Dispatch<React.SetStateAction<Invoice[]>>;
selectedInvoice: Invoice | null;
}
interface Invoice {
id: number;
name: string;
attributes: {};
senderEmail: string;
recipientEmail: string;
shippingAddress: string;
date: string;
dueDate: string;
invoiceNote: string;
description: string;
qty: number;
rate: number;
total: number;
}
const InvoiceForm: React.FC<InvoiceFormProps> = ({
onClose,
setInvoices,
selectedInvoice,
}) => {
const initialState = {
name: "",
senderEmail: "",
recipientEmail: "",
shippingAddress: "",
date: "",
dueDate: "",
invoiceNote: "",
description: "",
qty: 0,
rate: 0,
total: 0,
};
function reducer(
state = initialState,
{ field, value }: { field: string; value: any },
) {
return { ...state, [field]: value };
}
const [formFields, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
if (selectedInvoice) {
for (const [key, value] of Object.entries(selectedInvoice?.attributes)) {
dispatch({ field: key, value });
}
} else {
for (const [key, value] of Object.entries(initialState)) {
dispatch({ field: key, value });
}
}
}, [selectedInvoice]);
const handleInputChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
dispatch({ field: name, value });
};
useEffect(() => {
const { qty, rate } = formFields;
const total = qty * rate;
dispatch({ field: "total", value: total });
}, [formFields.qty, formFields.rate]);
const handleSendInvoice = async () => {
try {
const {
name,
senderEmail,
recipientEmail,
date,
dueDate,
shippingAddress,
invoiceNote,
description,
qty,
rate,
total,
} = formFields;
if (selectedInvoice) {
// Update an existing invoice
const data = await axios.put(
`http://localhost:1337/api/invoices/${selectedInvoice.id}`,
{
data: {
name,
senderEmail,
recipientEmail,
shippingAddress,
dueDate,
date,
invoiceNote,
description,
qty,
rate,
total,
},
},
);
console.log(data);
setInvoices((prev) =>
prev.map((inv) =>
inv.id === selectedInvoice.id ? { ...inv, ...formFields } : inv,
),
);
window.location.reload();
} else {
// Create a new invoice
const { data } = await axios.post(
"http://localhost:1337/api/invoices",
{
data: {
name,
senderEmail,
recipientEmail,
shippingAddress,
dueDate,
date,
invoiceNote,
description,
qty,
rate,
total,
},
},
);
console.log(data);
setInvoices((prev) => [...prev, data.data]);
}
onClose();
} catch (error) {
console.error(error);
}
};
return (
<>
<main className="fixed top-0 z-50 left-0 w-screen h-screen flex justify-center items-center bg-black bg-opacity-50">
<section className="relative lg:px-10 px-6 py-8 lg:mt-8 lg:w-[60%] bg-white shadow-md rounded px-8 pt-2 pb-8 mb-4">
<form className="pt-4">
<h2 className="text-lg font-medium mb-4">
{selectedInvoice ? "Edit Invoice" : "Create Invoice"}
</h2>
<button
className="absolute top-2 right-8 font-bold text-black cursor-pointer text-2xl"
onClick={onClose}
>
×
</button>
<div className="mb-4 flex flex-row justify-between">
<div className="flex flex-col w-[30%]">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="name"
>
Your name
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="name"
name="name"
type="text"
placeholder="Sender's name"
onChange={handleInputChange}
value={formFields.name}
required
/>
</div>
<div className="flex flex-col w-[30%]">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="senderEmail"
>
Your email address
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="senderEmail"
name="senderEmail"
type="email"
placeholder="Sender's email"
onChange={handleInputChange}
value={formFields.senderEmail}
required
/>
</div>
<div className="flex flex-col w-[30%]">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="recipientEmail"
>
Recipient's Email
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="recipientEmail"
name="recipientEmail"
type="email"
placeholder="Client's email address"
onChange={handleInputChange}
value={formFields.recipientEmail}
required
/>
</div>
</div>
<div className="mb-4 flex flex-row justify-between">
<div className="flex flex-col w-[45%]">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="date"
>
Date
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="date"
name="date"
type="date"
onChange={handleInputChange}
value={formFields.date}
required
/>
</div>
<div className="flex flex-col w-[45%]">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="dueDate"
>
Due Date
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="dueDate"
name="dueDate"
type="date"
onChange={handleInputChange}
value={formFields.dueDate}
required
/>
</div>
</div>
<div className="mb-4 flex flex-row justify-between">
<div className="flex flex-col w-[45%]">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="shippingAddress"
>
Shipping Address
</label>
<textarea
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="shippingAddress"
name="shippingAddress"
placeholder="Office address of recipient"
onChange={handleInputChange}
value={formFields.shippingAddress}
required
/>
</div>
<div className="flex flex-col w-[45%]">
<label
htmlFor="invoiceNote"
className="block text-gray-700 text-sm font-bold mb-2 w-full"
>
Invoice Note
</label>
<textarea
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="invoiceNote"
name="invoiceNote"
placeholder="Account details"
onChange={handleInputChange}
value={formFields.invoiceNote}
required
/>
</div>
</div>
<div className="flex justify-center items-center">
<label
htmlFor="description"
className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
>
Invoice Item
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="description"
name="description"
type="text"
placeholder="Reason for invoice"
onChange={handleInputChange}
value={formFields.description}
required
/>
</label>
<label
htmlFor="qty"
className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
>
Quantity
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="qty"
name="qty"
type="number"
onChange={handleInputChange}
value={formFields.qty}
required
/>
</label>
<label
htmlFor="rate"
className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
>
Rate
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="rate"
name="rate"
type="number"
onChange={handleInputChange}
value={formFields.rate}
required
/>
</label>
<div className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5">
<label>Total</label>
<div className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight">
{formFields.total}
</div>
</div>
</div>
<hr className="mt-5 border-1" />
<div className="mt-4 flex justify-center">
<button
type="button"
className="py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
onClick={handleSendInvoice}
>
{selectedInvoice ? "Update Invoice" : "Send Invoice"}
</button>
</div>
</form>
</section>
</main>
</>
);
};
export default InvoiceForm;
```
#### Code explanation
- After importing the necessary libraries, we defined the `InvoiceFormProps` interface by passing in three props. The first one `onClose` is the function to close the form. The second prop `setInvoices` is a function to update the invoices state in the parent component. The third one `selectedInvoice` is the invoice being edited if any has been selected.
- We then defined `Invoice` interfaces to type-check the props and state used in the component.
- The `initialState` object defines the initial state for the form fields. The `reducer` function updates the state based on the field and value provided to handle form field updates. The next line uses `useReducer` hook to manage form fields state, initializing with `initialState`.
- The first `useEffect` function is for pre-filling the form when user wants to edit the invoice. It runs whenever the `selectedInvoice` changes. If there is a `selectedInvoice` (indicating the user is editing an existing invoice), it populates the form fields with the invoice data. If there is no selected invoice, it resets the form fields to their initial values.
The `[selectedInvoice]` dependency array ensures this effect runs only when `selectedInvoice` changes.
- The second `useEffect` function is for calculating the total amount of the invoice, so that the total value of an invoice will be the quantity of the item multiplied by the rate being charged. This effect calculates the `total` whenever the `qty` or `rate` changes.
It extracts `qty` and `rate` from `formFields`, calculates the `total` by multiplying them, and then dispatches an action to update the `total` field in the state with the calculated value.
- The `handleInputChange` function handles changes to the form input fields and updates the corresponding state fields.
It destructures `name` and `value` from the event target and then dispatches an action to update the state field corresponding to `name` with the new `value`.
- The `handleSendInvoice` funtion handles the logic for sending an invoice to the Strapi backend. It sends a POST request to create a new invoice or a PUT request to update an existing one. It first extracts the necessary invoice details from `formFields` and then checks if `selectedInvoice` exists.
- If it exists, it means the user is updating an existing invoice. So it sends a PUT request to update the existing invoice on the server. It also updates the local state with the modified invoice data.
- If it does not exist, it means the user is creating a new invoice. So it sends a `POST` request to create a new invoice on the server. It then adds the newly created invoice to the local state.
- The `onClose` function is called to close the form whenever a user submits the form and the error handling to catch any errors during the request and log them to the console.
- The JSX for the invoice form is rendered. The form has a header that dynamically displays "Edit Invoice" or "Create Invoice" based on whether `selectedInvoice` is active.
The form is displayed with fields for the sender's name, email, recipient's email, dates, shipping address, invoice note, item description, quantity, rate, and total.
A button is provided to send the invoice, which triggers the `handleSendInvoice` function.
#### Display Invoices
This component is responsible for displaying the invoice and its contents. When users retrieve invoices from Strapi, they will be displayed here, along with buttons for generating, updating, deleting, and downloading them. The form for creating an invoice will also be displayed on this page.
In your `Invoices.tsx` component, paste these lines of code:
```js
import React, { useEffect, useState } from "react";
import axios from "axios";
import InvoiceForm from "./InvoiceForm";
interface Invoice {
id: number;
name: string;
senderEmail: string;
recipientEmail: string;
date: string;
dueDate: string;
shippingAddress: string;
invoiceNote: string;
description: string;
qty: number;
rate: number;
total: number;
}
const Invoices: React.FC = () => {
const [invoices, setInvoices] = useState<Invoice[]>([]);
const [isInvoiceFormOpen, setIsInvoiceFormOpen] = useState(false);
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
useEffect(() => {
const fetchInvoices = () => {
fetch("http://localhost:1337/api/invoices?populate=invoice")
.then((res) => {
if (!res.ok) {
throw new Error("Network response was not ok");
}
return res.json();
})
.then((data) => {
console.log("Fetched invoices:", data);
if (Array.isArray(data.data)) {
setInvoices(data.data);
} else {
console.error("Fetched data is not an array");
}
})
.catch((error) => {
console.error("Error fetching invoices:", error);
});
};
fetchInvoices();
}, []);
const handleOpenInvoiceForm = () => {
setSelectedInvoice(null);
setIsInvoiceFormOpen(true);
};
const handleCloseInvoiceForm = () => {
setSelectedInvoice(null);
setIsInvoiceFormOpen(false);
};
const handleEditInvoice = (invoice: Invoice) => {
console.log("Invoice being edited:", invoice);
setSelectedInvoice(invoice);
setIsInvoiceFormOpen(true);
};
const handleDeleteInvoice = async (id: number) => {
try {
alert("Are you sure you want to delete this invoice?");
await axios.delete(`http://localhost:1337/api/invoices/${id}`);
setInvoices(invoices.filter((invoice) => invoice.id !== id));
} catch (error) {
console.error(error);
}
};
return (
<div className="flex flex-col items-center justify-center">
<section className="w-[65%] flex flex-row justify-between py-4">
<h2 className="text-3xl text-gray-700 font-medium">INVOICE</h2>
<button
onClick={handleOpenInvoiceForm}
className="bg-green-500 p-2 w-30 text-white rounded-lg"
>
Create invoice
</button>
</section>
{isInvoiceFormOpen && (
<InvoiceForm
onClose={handleCloseInvoiceForm}
setInvoices={setInvoices}
selectedInvoice={selectedInvoice}
/>
)}
{invoices.length === 0 ? (
<p>No invoice yet.</p>
) : (
<div className="w-[70%]">
<div className="px-5 py-5 mx-auto">
{invoices.map((invoice) => (
<>
<div
className="flex flex-wrap border-t-2 border-b-2 border-gray-200 border-opacity-60"
key={invoice.id}
>
<div className="lg:w-1/3 md:w-full px-8 py-6 border-opacity-60">
<div>
<h2 className="text-base text-gray-900 font-medium mb-1">
Issued:
</h2>
<p className="leading-relaxed text-sm mb-4">
{invoice.attributes.date}
</p>
</div>
<div className="mt-12">
<h2 className="text-base text-gray-900 font-medium">
Due:
</h2>
<p className="leading-relaxed text-sm mb-4">
{invoice.attributes.dueDate}
</p>
</div>
</div>
<div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
<h2 className="text-base text-gray-900 font-medium mb-2">
Billed To:
</h2>
<div className="">
<h2 className=" text-gray-900 text-sm mb-1 font-medium">
Recipient's Email
</h2>
<p className="leading-relaxed text-sm mb-5">
{invoice.attributes.recipientEmail}
</p>
</div>
<div>
<h2 className=" text-gray-900 text-sm mb-1 font-medium">
Shipping Address
</h2>
<p className="leading-relaxed text-sm mb-4">
{invoice.attributes.shippingAddress}
</p>
</div>
</div>
<div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
<h2 className="text-base text-gray-900 font-medium mb-2">
From:
</h2>
<div className="">
<h2 className=" text-gray-900 text-sm mb-1 font-medium">
Sender's Name
</h2>
<p className="leading-relaxed text-sm mb-5">
{invoice.attributes.name}
</p>
</div>
<div>
<h2 className=" text-gray-900 text-sm mb-1 font-medium">
Sender's Email
</h2>
<p className="leading-relaxed text-sm mb-4">
{invoice.attributes.senderEmail}
</p>
</div>
</div>
</div>
<div className="w-full px-5 py-12 mx-auto">
<div className="flex flex-row justify-between border-b-2 border-gray-300">
<div>
<h2 className="text-lg font-medium text-gray-700 mb-2">
Invoice Item
</h2>
</div>
<div className="flex flex-row mb-2">
<p className="ml-2 text-lg font-medium text-gray-800">
Qty
</p>
<p className="ml-[6rem] text-lg font-medium text-gray-800">
Rate
</p>
<p className="ml-[6rem] text-lg font-medium text-gray-800">
Total
</p>
</div>
</div>
<div className="flex flex-row justify-between mt-4">
<div>
<h2 className="text-base text-gray-700 mb-4">
{invoice.attributes.description}
</h2>
</div>
<div className="flex flex-row mb-4">
<p className="ml-2 text-base text-gray-800">
{invoice.attributes.qty}
</p>
<p className="ml-[6rem] text-base text-gray-800">
${invoice.attributes.rate}
</p>
<p className="ml-[6rem] text-base text-gray-800">
${invoice.attributes.total}
</p>
</div>
</div>
<div className="grid justify-end pt-[2.5rem]">
<div className="flex flex-row justify-between">
<div>
<h2 className="text-lg font-medium text-gray-700 mb-4">
Tax (0%)
</h2>
</div>
<div className="flex flex-row">
<p className="ml-[10rem] text-base text-gray-800">
0.00
</p>
</div>
</div>
<div className="flex flex-row justify-between border-y-2 border-green-400">
<div className="pt-4">
<h2 className="text-lg font-medium text-gray-700 mb-4">
Amount due:
</h2>
</div>
<div className="flex flex-row pt-4">
<p className="ml-[10rem] text-lg font-medium text-gray-800">
${invoice.attributes.total}.00
</p>
</div>
</div>
</div>
</div>
<div className="flex flex-row justify-between w-full mt-1">
<div>
<button className="bg-blue-500 px-2 py-2 rounded text-white hover:bg-blue-600">
Download invoice
</button>
<button
className="bg-green-500 px-2 py-2 rounded text-white hover:bg-green-600 ml-4"
onClick={() => handleEditInvoice(invoice)}
>
Edit invoice
</button>
</div>
<div className="flex justify-end bg-red-400 px-2 py-2 rounded text-white hover:bg-red-500">
<button onClick={() => handleDeleteInvoice(invoice.id)}>
Delete invoice
</button>
</div>
</div>
</>
))}
</div>
</div>
)}
</div>
);
};
export default Invoices;
```
#### Code explanation
- First, we imported the `InvoiceForm` component which will be used here, along with the libraries installed.
- Since we're working with TypeScript, we set TypeScript interface to define the structure of the invoice object.
- We then set three states. The first state `const [invoices, setInvoices] = useState<Invoice[]>([]);` is an array to store fetched invoices. The second state `const [isInvoiceFormOpen, setIsInvoiceFormOpen] = useState(false);` will manage the visibility of the `InvoiceForm`. The third state `const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);` will store the invoice currently being edited.
- The `useEffect` hook is used to fetch invoices from the Strapi backend when the component mounts using the fetch API. The fetched data is stored in the invoices state.
- If the response is not OK, an error is thrown. If it's ok, the response is parsed as JSON. We also set a condition to check if the fetched invoices is an array so we'd be able to map through it to display the invoices. If is an array, it sets this data in the `invoices` state. If not, it logs an error.
- The `handleCloseInvoiceForm` and `handleCloseInvoiceForm` functions handle the opening and closing of the form modal.
- We defined the `handleEditInvoice` function that opens the invoice form pre-populated with the selected invoice's details for editing. It sets the `selectedInvoice` to the invoice to be edited and opens the invoice form by setting `isInvoiceFormOpen` to true.
- Next is the `handleDeleteInvoice` function that deletes an invoice by selecting its `id` and sending a `DELETE` request to the API. This removes or filters out the deleted invoice from the invoices state, and logs any error that will occur during the request.
- The component renders a list of invoices by mapping through the `invoices` array and rendering each invoice with a unique key. Each invoice displays details and buttons for editing and deleting.
If the `isInvoiceFormOpen` state is true, the `InvoiceForm` component is rendered for creating or editing invoices.
- This JSX will also conditionally render a message if there are no invoices to be displayed, otherwise renders the list of invoices. This is added so that the page doesn't look blank when there are no invoices to be displayed.
### Adding the jsPDF Download Functionality
To enable users download any created invoice in PDF format, let's utilize the jsPDF library. We'll also customize the PDF format a bit.
In your `Invoices.tsx` component:
#### Step 1: Import jsPDF and autotable Plugin
Import the jsPDF library and the autotable plugin in the component:
```js
import jsPDF from 'jspdf';
import 'jspdf-autotable';
```
#### Step 2: Allow Generating Tables in PDFs
The custom class `PDFWithAutoTable` is included to extend jsPDF to include the `autoTable` method for generating tables in PDFs.
```js
class PDFWithAutoTable extends jsPDF {
autoTable(options: any) {
// @ts-ignore
super.autoTable(options);
}
}
```
#### Step 3: Handle Invoice Download
The last step is to create a function to handle invoice download and add this function to the "Download invoice" button in the JSX.
The `handleDownloadPDF` function initializes a new `PDFWithAutoTable` document, sets the font size and style. It sets the invoice data to be included in the table '`const tableData[{ }]`'. It then uses `autoTable` property from the jspdf-autotable library to add the data to the PDF in a table format.
The last line saves the generated PDF with a filename that includes the invoice ID for easy identification.
```js
const handleDownloadPDF = (invoice: Invoice) => {
const doc = new PDFWithAutoTable();
// Set the font size and style
doc.setFontSize(12);
doc.setFont("helvetica", "normal");
// Tabular format of the invoice with corresponding information
const tableData = [
["Invoice id", `${invoice.id}`],
["Sender's name", `${invoice.attributes.name}`],
["Sender's email", `${invoice.attributes.senderEmail}`],
["Recipient's email", `${invoice.attributes.recipientEmail}`],
["Invoice date", `${invoice.attributes.date}`],
["Due date", `${invoice.attributes.dueDate}`],
["Shipping address", `${invoice.attributes.shippingAddress}`],
["Invoice note", `${invoice.attributes.invoiceNote}`],
["Invoice description", `${invoice.attributes.description}`],
["Item quantity", `(${invoice.attributes.qty})`],
["Rate", `${invoice.attributes.rate}`],
["Total", `${invoice.attributes.total}`],
];
// Customizing the table
doc.autoTable({
startY: 40,
head: [["Item", "Details"]],
body: tableData,
headStyles: { fontSize: 18, fontStyle: "bold" },
styles: { fontSize: 15, fontStyle: "semibold" },
});
// To save the PDF with a specific filename. In this case, with the invoice id
doc.save(`Invoice_${invoice.id}.pdf`);
};
```
You're free to customize the PDF any way you want to. Here's a list of [jsPDF classes](https://artskydj.github.io/jsPDF/docs/index.html).
Add an `onClick` event to the download button and you're set.
```html
<button onClick={() => handleDownloadPDF(invoice)}>
Download invoice
</button>
```
That's it! We've been able to build a functional invoicing app using Strapi as the backend to store the invoice data.
## Demo Time!
- Create invoice demo.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7gx5mpvh89frroc7tubl.gif)
- Edit invoice demo.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m72j6mmajqq5kbm2zdud.gif)
- Delete invoice demo.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bgrv0l97uskfh5zle39n.gif)
If you followed the steps in this tutorial, you should have a functional invoicing app where users can create, edit, and delete invoices on the frontend. You'll also be able to manage the data on the Strapi backend and also download the invoice in a PDF format.
## Conclusion
In this tutorial, we explored the steps involved in creating an invoicing app using technologies like Next.js for the frontend development, Strapi for the backend content management, and jsPDF for PDF generation.
We also learnt how to set up the development environment, creating the data collection in Strapi, how to connect the Strapi backend to the frontend, how to implement CRUD operations in Strapi, and how to integrate PDF generation functionality.
Using an invoicing app offers ready-made templates that allow quick generation of invoices and helps one keep track of outstanding invoices and due dates.
For reference, here's the [GitHub repository](https://github.com/OmaJuliet/Invoicing-App) where you can view the complete code for this project.
## Additional/Related Resources
* [Github Repo](https://github.com/OmaJuliet/Invoicing-App) for this project.
* [How to Build an Invoice Generator App with Next.js, Strapi & Tailwind CSS](https://strapi.io/blog/how-to-build-an-invoice-generator-app-with-next-js-strapi-and-tailwind-css).
* [How to Build a React PDF Invoice Generator App with refine and Strapi](https://strapi.io/blog/how-to-build-a-react-pdf-invoice-generator-app-with-refine-and-strapi).