DEV Community

Cover image for Building an Invoice Generator App with Next.js, Strapi & Tailwind CSS
Shada for Strapi

Posted on • Originally published at strapi.io

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

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
Enter fullscreen mode Exit fullscreen mode

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.


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 SettingUsers & Permissions PluginPublic 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

    npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Next, add the following import statement at the top of your pages/_app.js file.

    import 'tailwindcss/tailwind.css'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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: '',
        },
      ]);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 });
      };
Enter fullscreen mode Exit fullscreen mode

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);
      };
Enter fullscreen mode Exit fullscreen mode

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: '',
        },
      ]);
Enter fullscreen mode Exit fullscreen mode

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: '',
          },
        ]);
    }
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
      };
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
      };
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
        }
      };
Enter fullscreen mode Exit fullscreen mode

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();
      };
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)