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!
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.
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 |
|
recipientEmail |
|
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:
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.
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.
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:
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:
cd frontend
Install Dependencies
Next install the necessary dependencies for this project :
- Axios: Axios is a JavaScript library used to send asynchronous HTTP requests to REST endpoints. It's commonly used to perform CRUD operations. Install Axios using this command:
npm install axios
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.
jspdf-autotable: We'll use jsPDF along with the 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:
npm i jspdf jspdf-autotable
Start up your frontend app with the following command:
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
, andInvoiceForm.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 calledcomponents
. Create 2 components or files inside this folder and name them:Invoices.tsx
andInvoiceForm.tsx
.
TheInvoices
component will be where all the invoices created will be displayed. It will also be the main page of the application. TheInvoiceForm
component is for the form modal where users will have to input details to create or edit an invoice.In the
app
directory, locatepages.tsx
and replace the code with these lines of code:
'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:
"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 oneonClose
is the function to close the form. The second propsetInvoices
is a function to update the invoices state in the parent component. The third oneselectedInvoice
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. Thereducer
function updates the state based on the field and value provided to handle form field updates. The next line usesuseReducer
hook to manage form fields state, initializing withinitialState
.The first
useEffect
function is for pre-filling the form when user wants to edit the invoice. It runs whenever theselectedInvoice
changes. If there is aselectedInvoice
(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 whenselectedInvoice
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 thetotal
whenever theqty
orrate
changes.
It extractsqty
andrate
fromformFields
, calculates thetotal
by multiplying them, and then dispatches an action to update thetotal
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 destructuresname
andvalue
from the event target and then dispatches an action to update the state field corresponding toname
with the newvalue
.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 fromformFields
and then checks ifselectedInvoice
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 thehandleSendInvoice
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:
import React, { useEffect, useState } from "react";
import axios from "axios";
import InvoiceForm from "./InvoiceForm";
interface Invoice {
[x: string]: any;
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 stateconst [isInvoiceFormOpen, setIsInvoiceFormOpen] = useState(false);
will manage the visibility of theInvoiceForm
. The third stateconst [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
andhandleCloseInvoiceForm
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 theselectedInvoice
to the invoice to be edited and opens the invoice form by settingisInvoiceFormOpen
to true.Next is the
handleDeleteInvoice
function that deletes an invoice by selecting itsid
and sending aDELETE
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 theisInvoiceFormOpen
state is true, theInvoiceForm
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:
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.
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.
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.
Add an onClick
event to the download button and you're set.
<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!
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 where you can view the complete code for this project.
Top comments (8)
Wow, love the content, it was very insightful man! How have you been finding the jspdf library? I have used it in one of my previous projects but felt that it wasn't quite well-known/well-covered by others online
Yeah, that's true. jspdf isn't a very popular library. I came across it when I was doing a research on pdf libraries I can integrate into the project.
I've used it in one or two personal projects and it's been ok but I feel there should be room to allow for more advanced customization
Ahh, we came across it the same way haha!
What advanced customizations do you wish the library had?
More like advanced customization for the layout in general. I know there are options of listing out items and using tables, but some other layout customization option would be great
A great project.
To be frank, I've never used Strapi, so this is definitely new to me.
Anyway, it's a long read, so I will give it a full read later.
Thanks for this.
Yeah, you should definitely give Strapi a try. I hope you find it insightful
Great content man, appreciate the effort 👍
Great content Oma!!!...
May you grant me the go-ahead to dockerize this application for cloud native needs…and then make the pull request of the change…