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.
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:
In this section, we'll set up Strapi CMS as the backend platform to store the invoice data.
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
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!
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.
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.
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.
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.
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
Next install the necessary dependencies for this project :
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".
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 yourz 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:
1'use client'
2import Invoices from "../components/Invoices";
3
4function App() {
5 return (
6 <div className="p-5">
7 <Invoices />
8 </div>
9 );
10}
11export default App;
The main component which is the Invoices.tsx
component is imported and rendered as the main page of the application.
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.
In your InvoiceForm.tsx
component, paste these lines of code:
1"use client";
2import React, { ChangeEvent, useEffect, useReducer, useState } from "react";
3import axios from "axios";
4
5interface InvoiceFormProps {
6 onClose: () => void;
7 setInvoices: React.Dispatch<React.SetStateAction<Invoice[]>>;
8 selectedInvoice: Invoice | null;
9}
10
11interface Invoice {
12 id: number;
13 name: string;
14 attributes: {};
15 senderEmail: string;
16 recipientEmail: string;
17 shippingAddress: string;
18 date: string;
19 dueDate: string;
20 invoiceNote: string;
21 description: string;
22 qty: number;
23 rate: number;
24 total: number;
25}
26
27const InvoiceForm: React.FC<InvoiceFormProps> = ({
28 onClose,
29 setInvoices,
30 selectedInvoice,
31}) => {
32 const initialState = {
33 name: "",
34 senderEmail: "",
35 recipientEmail: "",
36 shippingAddress: "",
37 date: "",
38 dueDate: "",
39 invoiceNote: "",
40 description: "",
41 qty: 0,
42 rate: 0,
43 total: 0,
44 };
45
46 function reducer(
47 state = initialState,
48 { field, value }: { field: string; value: any },
49 ) {
50 return { ...state, [field]: value };
51 }
52
53 const [formFields, dispatch] = useReducer(reducer, initialState);
54
55 useEffect(() => {
56 if (selectedInvoice) {
57 for (const [key, value] of Object.entries(selectedInvoice?.attributes)) {
58 dispatch({ field: key, value });
59 }
60 } else {
61 for (const [key, value] of Object.entries(initialState)) {
62 dispatch({ field: key, value });
63 }
64 }
65 }, [selectedInvoice]);
66
67 const handleInputChange = (
68 e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
69 ) => {
70 const { name, value } = e.target;
71 dispatch({ field: name, value });
72 };
73
74 useEffect(() => {
75 const { qty, rate } = formFields;
76 const total = qty * rate;
77 dispatch({ field: "total", value: total });
78 }, [formFields.qty, formFields.rate]);
79
80 const handleSendInvoice = async () => {
81 try {
82 const {
83 name,
84 senderEmail,
85 recipientEmail,
86 date,
87 dueDate,
88 shippingAddress,
89 invoiceNote,
90 description,
91 qty,
92 rate,
93 total,
94 } = formFields;
95
96 if (selectedInvoice) {
97 // Update an existing invoice
98 const data = await axios.put(
99 `http://localhost:1337/api/invoices/${selectedInvoice.id}`,
100 {
101 data: {
102 name,
103 senderEmail,
104 recipientEmail,
105 shippingAddress,
106 dueDate,
107 date,
108 invoiceNote,
109 description,
110 qty,
111 rate,
112 total,
113 },
114 },
115 );
116 console.log(data);
117 setInvoices((prev) =>
118 prev.map((inv) =>
119 inv.id === selectedInvoice.id ? { ...inv, ...formFields } : inv,
120 ),
121 );
122 window.location.reload();
123 } else {
124 // Create a new invoice
125 const { data } = await axios.post(
126 "http://localhost:1337/api/invoices",
127 {
128 data: {
129 name,
130 senderEmail,
131 recipientEmail,
132 shippingAddress,
133 dueDate,
134 date,
135 invoiceNote,
136 description,
137 qty,
138 rate,
139 total,
140 },
141 },
142 );
143 console.log(data);
144 setInvoices((prev) => [...prev, data.data]);
145 }
146
147 onClose();
148 } catch (error) {
149 console.error(error);
150 }
151 };
152
153 return (
154 <>
155 <main className="fixed top-0 z-50 left-0 w-screen h-screen flex justify-center items-center bg-black bg-opacity-50">
156 <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">
157 <form className="pt-4">
158 <h2 className="text-lg font-medium mb-4">
159 {selectedInvoice ? "Edit Invoice" : "Create Invoice"}
160 </h2>
161 <button
162 className="absolute top-2 right-8 font-bold text-black cursor-pointer text-2xl"
163 onClick={onClose}
164 >
165 ×
166 </button>
167
168 <div className="mb-4 flex flex-row justify-between">
169 <div className="flex flex-col w-[30%]">
170 <label
171 className="block text-gray-700 text-sm font-bold mb-2"
172 htmlFor="name"
173 >
174 Your name
175 </label>
176 <input
177 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
178 id="name"
179 name="name"
180 type="text"
181 placeholder="Sender's name"
182 onChange={handleInputChange}
183 value={formFields.name}
184 required
185 />
186 </div>
187
188 <div className="flex flex-col w-[30%]">
189 <label
190 className="block text-gray-700 text-sm font-bold mb-2"
191 htmlFor="senderEmail"
192 >
193 Your email address
194 </label>
195 <input
196 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
197 id="senderEmail"
198 name="senderEmail"
199 type="email"
200 placeholder="Sender's email"
201 onChange={handleInputChange}
202 value={formFields.senderEmail}
203 required
204 />
205 </div>
206
207 <div className="flex flex-col w-[30%]">
208 <label
209 className="block text-gray-700 text-sm font-bold mb-2"
210 htmlFor="recipientEmail"
211 >
212 Recipient's Email
213 </label>
214 <input
215 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"
216 id="recipientEmail"
217 name="recipientEmail"
218 type="email"
219 placeholder="Client's email address"
220 onChange={handleInputChange}
221 value={formFields.recipientEmail}
222 required
223 />
224 </div>
225 </div>
226
227 <div className="mb-4 flex flex-row justify-between">
228 <div className="flex flex-col w-[45%]">
229 <label
230 className="block text-gray-700 text-sm font-bold mb-2"
231 htmlFor="date"
232 >
233 Date
234 </label>
235 <input
236 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"
237 id="date"
238 name="date"
239 type="date"
240 onChange={handleInputChange}
241 value={formFields.date}
242 required
243 />
244 </div>
245
246 <div className="flex flex-col w-[45%]">
247 <label
248 className="block text-gray-700 text-sm font-bold mb-2"
249 htmlFor="dueDate"
250 >
251 Due Date
252 </label>
253 <input
254 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"
255 id="dueDate"
256 name="dueDate"
257 type="date"
258 onChange={handleInputChange}
259 value={formFields.dueDate}
260 required
261 />
262 </div>
263 </div>
264
265 <div className="mb-4 flex flex-row justify-between">
266 <div className="flex flex-col w-[45%]">
267 <label
268 className="block text-gray-700 text-sm font-bold mb-2"
269 htmlFor="shippingAddress"
270 >
271 Shipping Address
272 </label>
273 <textarea
274 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
275 id="shippingAddress"
276 name="shippingAddress"
277 placeholder="Office address of recipient"
278 onChange={handleInputChange}
279 value={formFields.shippingAddress}
280 required
281 />
282 </div>
283
284 <div className="flex flex-col w-[45%]">
285 <label
286 htmlFor="invoiceNote"
287 className="block text-gray-700 text-sm font-bold mb-2 w-full"
288 >
289 Invoice Note
290 </label>
291 <textarea
292 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"
293 id="invoiceNote"
294 name="invoiceNote"
295 placeholder="Account details"
296 onChange={handleInputChange}
297 value={formFields.invoiceNote}
298 required
299 />
300 </div>
301 </div>
302
303 <div className="flex justify-center items-center">
304 <label
305 htmlFor="description"
306 className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
307 >
308 Invoice Item
309 <input
310 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"
311 id="description"
312 name="description"
313 type="text"
314 placeholder="Reason for invoice"
315 onChange={handleInputChange}
316 value={formFields.description}
317 required
318 />
319 </label>
320
321 <label
322 htmlFor="qty"
323 className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
324 >
325 Quantity
326 <input
327 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"
328 id="qty"
329 name="qty"
330 type="number"
331 onChange={handleInputChange}
332 value={formFields.qty}
333 required
334 />
335 </label>
336
337 <label
338 htmlFor="rate"
339 className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
340 >
341 Rate
342 <input
343 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"
344 id="rate"
345 name="rate"
346 type="number"
347 onChange={handleInputChange}
348 value={formFields.rate}
349 required
350 />
351 </label>
352
353 <div className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5">
354 <label>Total</label>
355 <div className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight">
356 {formFields.total}
357 </div>
358 </div>
359 </div>
360
361 <hr className="mt-5 border-1" />
362
363 <div className="mt-4 flex justify-center">
364 <button
365 type="button"
366 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"
367 onClick={handleSendInvoice}
368 >
369 {selectedInvoice ? "Update Invoice" : "Send Invoice"}
370 </button>
371 </div>
372 </form>
373 </section>
374 </main>
375 </>
376 );
377};
378
379export default InvoiceForm;
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.
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:
1import React, { useEffect, useState } from "react";
2import axios from "axios";
3import InvoiceForm from "./InvoiceForm";
4
5interface Invoice {
6 [x: string]: any;
7 id: number;
8 name: string;
9 senderEmail: string;
10 recipientEmail: string;
11 date: string;
12 dueDate: string;
13 shippingAddress: string;
14 invoiceNote: string;
15 description: string;
16 qty: number;
17 rate: number;
18 total: number;
19}
20
21const Invoices: React.FC = () => {
22 const [invoices, setInvoices] = useState<Invoice[]>([]);
23 const [isInvoiceFormOpen, setIsInvoiceFormOpen] = useState(false);
24 const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
25
26 useEffect(() => {
27 const fetchInvoices = () => {
28 fetch("http://localhost:1337/api/invoices?populate=invoice")
29 .then((res) => {
30 if (!res.ok) {
31 throw new Error("Network response was not ok");
32 }
33 return res.json();
34 })
35 .then((data) => {
36 console.log("Fetched invoices:", data);
37 if (Array.isArray(data.data)) {
38 setInvoices(data.data);
39 } else {
40 console.error("Fetched data is not an array");
41 }
42 })
43 .catch((error) => {
44 console.error("Error fetching invoices:", error);
45 });
46 };
47
48 fetchInvoices();
49 }, []);
50
51 const handleOpenInvoiceForm = () => {
52 setSelectedInvoice(null);
53 setIsInvoiceFormOpen(true);
54 };
55
56 const handleCloseInvoiceForm = () => {
57 setSelectedInvoice(null);
58 setIsInvoiceFormOpen(false);
59 };
60
61 const handleEditInvoice = (invoice: Invoice) => {
62 console.log("Invoice being edited:", invoice);
63 setSelectedInvoice(invoice);
64 setIsInvoiceFormOpen(true);
65 };
66
67 const handleDeleteInvoice = async (id: number) => {
68 try {
69 alert("Are you sure you want to delete this invoice?");
70 await axios.delete(`http://localhost:1337/api/invoices/${id}`);
71 setInvoices(invoices.filter((invoice) => invoice.id !== id));
72 } catch (error) {
73 console.error(error);
74 }
75 };
76
77 return (
78 <div className="flex flex-col items-center justify-center">
79 <section className="w-[65%] flex flex-row justify-between py-4">
80 <h2 className="text-3xl text-gray-700 font-medium">INVOICE</h2>
81 <button
82 onClick={handleOpenInvoiceForm}
83 className="bg-green-500 p-2 w-30 text-white rounded-lg"
84 >
85 Create invoice
86 </button>
87 </section>
88
89 {isInvoiceFormOpen && (
90 <InvoiceForm
91 onClose={handleCloseInvoiceForm}
92 setInvoices={setInvoices}
93 selectedInvoice={selectedInvoice}
94 />
95 )}
96
97 {invoices.length === 0 ? (
98 <p>No invoice yet.</p>
99 ) : (
100 <div className="w-[70%]">
101 <div className="px-5 py-5 mx-auto">
102 {invoices.map((invoice) => (
103 <>
104 <div
105 className="flex flex-wrap border-t-2 border-b-2 border-gray-200 border-opacity-60"
106 key={invoice.id}
107 >
108 <div className="lg:w-1/3 md:w-full px-8 py-6 border-opacity-60">
109 <div>
110 <h2 className="text-base text-gray-900 font-medium mb-1">
111 Issued:
112 </h2>
113 <p className="leading-relaxed text-sm mb-4">
114 {invoice.attributes.date}
115 </p>
116 </div>
117 <div className="mt-12">
118 <h2 className="text-base text-gray-900 font-medium">
119 Due:
120 </h2>
121 <p className="leading-relaxed text-sm mb-4">
122 {invoice.attributes.dueDate}
123 </p>
124 </div>
125 </div>
126
127 <div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
128 <h2 className="text-base text-gray-900 font-medium mb-2">
129 Billed To:
130 </h2>
131 <div className="">
132 <h2 className=" text-gray-900 text-sm mb-1 font-medium">
133 Recipient's Email
134 </h2>
135 <p className="leading-relaxed text-sm mb-5">
136 {invoice.attributes.recipientEmail}
137 </p>
138 </div>
139
140 <div>
141 <h2 className=" text-gray-900 text-sm mb-1 font-medium">
142 Shipping Address
143 </h2>
144 <p className="leading-relaxed text-sm mb-4">
145 {invoice.attributes.shippingAddress}
146 </p>
147 </div>
148 </div>
149
150 <div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
151 <h2 className="text-base text-gray-900 font-medium mb-2">
152 From:
153 </h2>
154 <div className="">
155 <h2 className=" text-gray-900 text-sm mb-1 font-medium">
156 Sender's Name
157 </h2>
158 <p className="leading-relaxed text-sm mb-5">
159 {invoice.attributes.name}
160 </p>
161 </div>
162
163 <div>
164 <h2 className=" text-gray-900 text-sm mb-1 font-medium">
165 Sender's Email
166 </h2>
167 <p className="leading-relaxed text-sm mb-4">
168 {invoice.attributes.senderEmail}
169 </p>
170 </div>
171 </div>
172 </div>
173
174 <div className="w-full px-5 py-12 mx-auto">
175 <div className="flex flex-row justify-between border-b-2 border-gray-300">
176 <div>
177 <h2 className="text-lg font-medium text-gray-700 mb-2">
178 Invoice Item
179 </h2>
180 </div>
181
182 <div className="flex flex-row mb-2">
183 <p className="ml-2 text-lg font-medium text-gray-800">
184 Qty
185 </p>
186 <p className="ml-[6rem] text-lg font-medium text-gray-800">
187 Rate
188 </p>
189 <p className="ml-[6rem] text-lg font-medium text-gray-800">
190 Total
191 </p>
192 </div>
193 </div>
194
195 <div className="flex flex-row justify-between mt-4">
196 <div>
197 <h2 className="text-base text-gray-700 mb-4">
198 {invoice.attributes.description}
199 </h2>
200 </div>
201
202 <div className="flex flex-row mb-4">
203 <p className="ml-2 text-base text-gray-800">
204 {invoice.attributes.qty}
205 </p>
206 <p className="ml-[6rem] text-base text-gray-800">
207 ${invoice.attributes.rate}
208 </p>
209 <p className="ml-[6rem] text-base text-gray-800">
210 ${invoice.attributes.total}
211 </p>
212 </div>
213 </div>
214
215 <div className="grid justify-end pt-[2.5rem]">
216 <div className="flex flex-row justify-between">
217 <div>
218 <h2 className="text-lg font-medium text-gray-700 mb-4">
219 Tax (0%)
220 </h2>
221 </div>
222
223 <div className="flex flex-row">
224 <p className="ml-[10rem] text-base text-gray-800">
225 0.00
226 </p>
227 </div>
228 </div>
229
230 <div className="flex flex-row justify-between border-y-2 border-green-400">
231 <div className="pt-4">
232 <h2 className="text-lg font-medium text-gray-700 mb-4">
233 Amount due:
234 </h2>
235 </div>
236
237 <div className="flex flex-row pt-4">
238 <p className="ml-[10rem] text-lg font-medium text-gray-800">
239 ${invoice.attributes.total}.00
240 </p>
241 </div>
242 </div>
243 </div>
244 </div>
245
246 <div className="flex flex-row justify-between w-full mt-1">
247 <div>
248 <button className="bg-blue-500 px-2 py-2 rounded text-white hover:bg-blue-600">
249 Download invoice
250 </button>
251
252 <button
253 className="bg-green-500 px-2 py-2 rounded text-white hover:bg-green-600 ml-4"
254 onClick={() => handleEditInvoice(invoice)}
255 >
256 Edit invoice
257 </button>
258 </div>
259
260 <div className="flex justify-end bg-red-400 px-2 py-2 rounded text-white hover:bg-red-500">
261 <button onClick={() => handleDeleteInvoice(invoice.id)}>
262 Delete invoice
263 </button>
264 </div>
265 </div>
266 </>
267 ))}
268 </div>
269 </div>
270 )}
271 </div>
272 );
273};
274
275export default Invoices;
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.
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:
Import the jsPDF library and the autotable plugin in the component:
1import jsPDF from 'jspdf';
2import 'jspdf-autotable';
The custom class PDFWithAutoTable
is included to extend jsPDF to include the autoTable
method for generating tables in PDFs.
1class PDFWithAutoTable extends jsPDF {
2 autoTable(options: any) {
3 // @ts-ignore
4 super.autoTable(options);
5 }
6}
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.
1const handleDownloadPDF = (invoice: Invoice) => {
2 const doc = new PDFWithAutoTable();
3
4 // Set the font size and style
5 doc.setFontSize(12);
6 doc.setFont("helvetica", "normal");
7
8 // Tabular format of the invoice with corresponding information
9 const tableData = [
10 ["Invoice id", `${invoice.id}`],
11 ["Sender's name", `${invoice.attributes.name}`],
12 ["Sender's email", `${invoice.attributes.senderEmail}`],
13 ["Recipient's email", `${invoice.attributes.recipientEmail}`],
14 ["Invoice date", `${invoice.attributes.date}`],
15 ["Due date", `${invoice.attributes.dueDate}`],
16 ["Shipping address", `${invoice.attributes.shippingAddress}`],
17 ["Invoice note", `${invoice.attributes.invoiceNote}`],
18 ["Invoice description", `${invoice.attributes.description}`],
19 ["Item quantity", `(${invoice.attributes.qty})`],
20 ["Rate", `${invoice.attributes.rate}`],
21 ["Total", `${invoice.attributes.total}`],
22 ];
23
24 // Customizing the table
25 doc.autoTable({
26 startY: 40,
27 head: [["Item", "Details"]],
28 body: tableData,
29 headStyles: { fontSize: 18, fontStyle: "bold" },
30 styles: { fontSize: 15, fontStyle: "semibold" },
31 });
32
33 // To save the PDF with a specific filename. In this case, with the invoice id
34 doc.save(`Invoice_${invoice.id}.pdf`);
35};
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.
1<button onClick={() => handleDownloadPDF(invoice)}>
2 Download invoice
3</button>
That's it! We've been able to build a functional invoicing app using Strapi as the backend to store the invoice data.
Create invoice demo.
Edit invoice demo.
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.
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.
Juliet is a developer and technical writer whose work aims to break down complex technical concepts and empower developers of all levels.