How to Build an Invoice Generator App with Next.js, Strapi & Tailwind CSS
In the tutorial, we will build REST API with Strapi, post our invoice items from our Nex.js app to the backend, and then give users the option to print the Invoice and save it to their device. We will also work with Tailwind CSS to style our application, so it looks elegant.
Let take a look at a demo of what we are going to be building throughout this article:
Prerequisites
This tutorial is beginner-friendly, but to follow along, you will need to have a basic knowledge of the following:
- Basic knowledge of React
- Basic understanding of Strapi - get started here.
- Node.js installed on your computer.
Setting up the Backend
This section will focus on scaffolding our Strapi project, creating our invoices collections, and then making them accessible to our frontend application.
Creating a Strapi Project To scaffold a Strapi project, run either of the following commands in your terminal.
yarn create strapi-app invoice-generator-api --quickstart
# OR
npx create-strapi-app invoice-generator-api --quickstart
When this is done, cd
into your newly created Strapi project and run yarn develop
or npm run develop
to start the development server of our Strapi project.
Creating Invoice Collection
Next, we will create a new Collection Type that will store the data for each invoice item sent from our Next.js app.
Let's create a Collection Type called Invoice
with the necessary fields. To do this, navigate to Content-Types Builder, localhost:1337/admin/plugins/content-type-…, on the admin dashboard and click on the Create new collection type
button and fill invoice
as the display name.
Click on the "Add another field" button, add a field with the named sender
, and choose an Email
type. This will receive the sender of the invoice email. Add another field name of billTo
and select a type of Text
.
Next, add an Email
type with the name shipTo
, dueDate with a type of Date
, note
with a type of Text
, invoiceItemDetails
with a type of JSON
, and lastly total
with. type of Number
Next, add an Email
type with the name shipTo
, dueDate with a type of Date
, note
with a type of Text
, invoiceItemDetails
with a type of JSON
, and lastly, total
with type of Number
.
When you are done setting up the fields in your collections, you should end up with this:
Setting up Roles and Permissions
Next, we need to set up our roles and permission to have access to our data from our Next.js App. To do this, navigate to Setting
→ Users & Permissions Plugin
→ Public
and then tick on the select all checkbox under the Permissions
section and click on the Save
button at the right top corner:
Bootstrap a Next.js Project
We have successfully created our API with Strapi. Let's move over to the frontend of our app and build the interface and frontend functionality with Next.js. To bootstrap a Next.js app, run either of the following commands:
npx create-next-app invoice-generator
# or
yarn create next-app invoice-generator
Adding Tailwind CSS to our project
We will be using Tailwind CSS to build out our app's interface and install it in our Next.js app. Run the following command at the root of your newly created Next.js app on your terminal to install Tailwind CSS:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
After that, run the following command to create the necessary config files for tailwind:
npx tailwindcss init -p
Next, add the following import
statement at the top of your pages/_app.js
file.
import 'tailwindcss/tailwind.css'
Building the frontend
Building the invoice interface
Now that we have the interface of our app with Next.js and style it with Tailwind CSS. This is the part of the interface we are going to be building first:
Add the following line of code to your index.js
:
<form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="sender"
>
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="sender"
name="sender"
type="email"
required
placeholder="Who is this invoice from? (required)"
// onChange={handleInputChange}
/>
<label
className="block text-gray-700 text-sm font-bold my-3"
htmlFor="billTo"
>
Bill To
</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="billTo"
name="billTo"
type="email"
required
placeholder="Who is this invoice to? (required)"
// onChange={handleInputChange}
/>
</div>
<div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="shipTo"
>
Ship To
</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="shipTo"
name="shipTo"
type="email"
required
placeholder="Client's email"
// onChange={handleInputChange}
/>
</div>
</form>
Notice how the *onChange*
event handler is commented out on the code above. This is because we've not implemented the handler yet. We will do that later.
Next, let's build this row of our Invoice where users can add multiple invoice items, the quantity, and price as well and remove unwanted invoice items:
Add the following lines of code inside of the form we created above:
{invoiceFields.map((invoiceField, i) => (
<div
className="flex justify-center items-center"
key={`${invoiceField}~${i}`}
>
<label
className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
htmlFor={`${invoiceField.itemDescription}~${i}`}
>
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={`${invoiceField.itemDescription}~${i}`}
name="itemDescription"
type="text"
spellCheck="false"
// value={invoiceField.itemDescription}
// onChange={(event) => handleChange(i, event)}
/>
</label>
<label
className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
htmlFor={`${invoiceField.qty}~${i}`}
>
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={`${invoiceField.qty}~${i}`}
name="qty"
type="number"
spellCheck="false"
// value={invoiceField.qty}
// onChange={(event) => handleChange(i, event)}
/>
</label>
<label
className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
htmlFor={`${invoiceField.price}~${i}`}
>
Unit Price
<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={`${invoiceField.price}~${i}`}
name="price"
type="tel"
spellCheck="false"
// value={invoiceField.price}
// onChange={(event) => handleChange(i, event)}
/>
</label>
<button
className="bg-red-500 hover:bg-red-700 h-8 px-5 py-3 flex items-center justify-center text-white font-bold rounded focus:outline-none focus:shadow-outline"
type="button"
// onClick={() => handleRemoveInvoice(i)}
>
Remove
</button>
</div>
))}
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="button"
// onClick={addInvoiceItem}
>
Add Item
</button>
Notice we have some lines of code commented out in our form up there. This is what we will use to get the values from our input field. We will also create these event handlers later in this section.
Next, let's create the state in our app where the invoice items we've created from our form will come from. Don't forget to import useState
from react
:
const [invoiceFields, setInvoiceFields] = useState([
{
itemDescription: '',
qty: '',
price: '',
},
]);
Let's move on to creating the last part of our invoice user interface:
Add the following lines of code to get the interface above. We will implement the feature to get the data from input field and manage state as well as sending it to the API we created later.
<div className="my-6 flex flex-col">
<label
htmlFor="note"
className="block text-gray-700 text-sm font-bold mb-2 w-full"
>
Invoice Notes
</label>
<textarea
id="note"
name="note"
// onChange={handleInputChange}
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"
/>
</div>
<div className="mb-6 flex justify-between font-bold text-xl">
<p>Total:</p>
{/* <p>{total}</p> */}
</div>
<div className="flex items-center justify-between">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="button"
// onClick={handleSendInvoice}
>
Send Invoice
</button>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="button"
// onClick={handlePrintInvoice}
>
Download Invoice
</button>
</div>
Get values from our invoice form fields
This section will get the values from our React form using the useReducer and useState hooks. Let’s create a reducer function and then dispatch the function to get the values from our input fields:
// export default function Home() {
const initialState = {
sender: '',
billTo: '',
shipTo: '',
dueDate: '',
note: '',
};
function reducer(state = initialState, { field, value }) {
return { ...state, [field]: value };
}
const [formFields, dispatch] = useReducer(reducer, initialState);
Now that we've created our reducer function let's use the dispatch function returned to us by the useReducer
hook to get the value of our input fields. To do this, we will create a function and call the dispatch method there and add an onChange
event to our input field that will call our function:
// we will add this function to the onChange event in the input field in our invoice
const handleInputChange = (e) => {
dispatch({ field: e.target.name, value: e.target.value });
};
We already have the handleInputChange
function we just created attached to the form field in the invoice input field, so uncomment it out, and you should get access to the values of the input fields.
Let's create a separate event handler to get the value of the input fields of this dynamic section in our input field:
const handleChange = (index, event) => {
const values = [...invoiceFields];
if (event.target.name === 'itemDescription') {
values[index].itemDescription = event.target.value;
} else if (event.target.name === 'qty') {
values[index].qty = event.target.value;
} else if (event.target.name === 'price') {
values[index].price = event.target.value;
}
setInvoiceFields(values);
};
Above, we are checking for the name of the input field the user interacts with and then change the value of the input field to be in sync with what we have in our useState
:
const [invoiceFields, setInvoiceFields] = useState([
{
itemDescription: '',
qty: '',
price: '',
},
]);
We are checking if the name
attribute of the input field is that of the qty
. For example, we want to set the index of that particular input field in our state to be the value of what the user enters.
Add invoice items
Let's add the functionality to add invoice items to our array of invoiceFields
in our useState
hook.
Here, we call the setInvoiceFields
function returned from the useState
hook and then used the spread operator to copy what we have in our state and then add the object to what we have already in our state.
const addInvoiceItem = () => {
setInvoiceFields([
...invoiceFields,
{
itemDescription: '',
qty: '',
price: '',
},
]);
}
Next, we have to call our function and make sure the total updates only when necessary. To do this, we will call the getTotal
function in the useEffect hook.
useEffect(() => {
getTotal();
}, [total]);
Remove invoice items
Let's add the functionality to remove invoice items from our array of invoiceFields
in our useState
hook.
To do this, we need to bass the index
of the invoice item clicked by the user to our click function as seen below:
<button className="bg-red-500 hover:bg-red-700 h-8 px-5 py-3 flex items-center justify-center text-white font-bold rounded focus:outline-none focus:shadow-outline"
type="button"
onClick={() => handleRemoveInvoice(i)}
>
Remove
</button>
Notice this line of code; if (values.length === 1) return false;
we don’t want to remove the invoice item if it’s the only item in our invoice. Then we want to use the splice array method to remove the item that the user clicks and update the state.
const handleRemoveInvoice = (index) => {
const values = [...invoiceFields];
if (values.length === 1) return false;
values.splice(index, 1);
setInvoiceFields(values);
};
Invoice Computation Item
Now let's compute the total price for the invoice item users will add. To achieve this, we will use the useState
hook.
const [total, setTotal] = useState(0);
Notice we are using the forEach
method iterate over each of the items in the invoiceFields
array and get the quantity and price multiply both numbers and call the setTotal
method and pass the computed values to it.
const getTotal = () => {
let computedTotal = 0;
invoiceFields.forEach((field) => {
const quantityNumber = parseFloat(field.qty);
const rateNumber = parseFloat(field.price);
const amount =
quantityNumber && rateNumber ? quantityNumber * rateNumber : 0;
computedTotal += amount;
});
return setTotal(computedTotal);
};
Connecting to our Strapi backend
This section will send our data from our frontend to the backend we've set up earlier on. We will use Axios to make HTTP requests.
Let's go ahead and install Axios. Run the following command in the terminal to install the package:
npm install axios
# or
yarn add axios
Then we need to send the invoice items to our server. Notice the window.print()
method we are using. We use it to open the print dialog on our browser after we’ve sent the request to our API and gotten a response.
const handleSendInvoice = async () => {
try {
let { billTo, dueDate, note, sender, shipTo } = formFields;
const { data } = await axios.post('localhost:1337/invoices', {
billTo,
dueDate,
note,
sender,
shipTo,
invoiceItemDetails: invoiceFields,
total,
});
window.print();
} catch (error) {
console.error(error);
}
};
Generating Invoice
Let's create a handler where users can generate invoices without sending the request to the API. To do this, you'll have to fill in your invoice details and click on the "Download invoice" button. This button will trigger an onClick
event that calls the window.print()
method.
const handlePrintInvoice = () => {
window.print();
};
Conclusion
In this article, we've seen how powerful and very easy to use Strapi is. We've seen how to add various field types ranging from email, Date, Number, and more to our collection. Setting up a backend projec is like a walk in the park, very simple and easy. By just creating our collections, Strapi will provide us with endpoints we need following best web practices.
We also built our frontend application with Next.js and styled our invoice app with Tailwind CSS.
You can find the complete code used in this tutorial for the frontend app here, and the backend code is available on GitHub. You can also find me on Twitter, LinkedIn, and GitHub.
Feel free to drop a comment to let me know what you thought of this article.