Building an Invoice Generator App with Next.js, Strapi & Tailwind CSS
Tags: nextjs, tailwindcss, strapi, app
Published: 2021-09-21 16:37:46


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](https://tailwindcss.com/) 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:

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627290571369_ezgif.com-gif-maker+1.gif)



## 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](https://reactjs.org/)
- Basic understanding of Strapi - [get started here](https://strapi.io/documentation/developer-docs/latest/getting-started/introduction.html).
- [Node.js](https://nodejs.org/en/) installed on your computer.

## Setting up the Backend

This section will focus on scaffolding our [Strapi](https://strapi.io/) 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.

```bash
    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, http://localhost:1337/admin/plugins/content-type-builder, on the admin dashboard and click on the `Create new collection type` button and fill `invoice` as the display name.

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627318528706_Screen+Shot+2021-07-26+at+17.53.10.png)

****
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`.

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627318964789_Screen+Shot+2021-07-26+at+18.02.22.png)


When you are done setting up the fields in your collections, you should end up with this:

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627319217604_Screen+Shot+2021-07-26+at+18.05.03.png)


**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:

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627319434045_Screen+Shot+2021-07-26+at+18.05.03.png)

## 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](https://nextjs.org/). To bootstrap a Next.js app, run either of the following commands:

```bash
    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:

```bash
    npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
```

After that, run the following command to create the necessary config files for tailwind:

```bash
    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:

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627220716217_Screen+Shot+2021-07-25+at+14.45.02.png)


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:

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627221378240_Screen+Shot+2021-07-25+at+14.56.04.png)


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:

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627222318551_Screen+Shot+2021-07-25+at+15.11.46.png)


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](https://blog.logrocket.com/guide-to-react-usereducer-hook/) and [useState](https://reactjs.org/docs/hooks-state.html) 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:

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627224204788_Screen+Shot+2021-07-25+at+14.56.04.png)

```
      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.


![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627278911287_Screen+Shot+2021-07-26+at+06.54.56.png)


Here, we call the `setInvoiceFields` function returned from the `useState` hook and then used the [spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) 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](https://reactjs.org/docs/hooks-effect.html) 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.

![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627225136301_Screen+Shot+2021-07-25+at+15.58.44.png)


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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/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](https://axios-http.com/) to make HTTP requests.

Let's go ahead and install Axios. Run the following command in the terminal to install the package:

```bash
    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('http://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.


![](https://paper-attachments.dropbox.com/s_B40D2BC0485F66A854F26E124056DFA7AF05A74CFA3435CE44755D504B8AD089_1627290245832_ezgif.com-gif-maker.gif)


```
    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](https://github.com/ejirocodes/Invoice-Generator), and the backend code is available on [GitHub](https://github.com/ejirocodes/Invoice-generator-backend). You can also find me on [Twitter](https://twitter.com/ejirocodes), [LinkedIn](https://www.linkedin.com/in/ejiro-asiuwhu/), and [GitHub](https://github.com/ejirocodes). 

Feel free to drop a comment to let me know what you thought of this article.