DEV Community

Cover image for Building an Invoice Generator using Cloudinary and Xata
Moronfolu Olufunke for Hackmamba

Posted on

Building an Invoice Generator using Cloudinary and Xata

Invoices are documents tended by a service provider to the recipient of the service. Crafting a detailed invoice regarding the services rendered, the price, and all the needed details are fast becoming a vital skill of business owners as the ease of conducting transactions also determines the scope of future business encounters.

This article will discuss creating an invoice generator in Vue using Cloudinary and Xata.

Cloudinary is a cloud-based video and image management platform that offers services for managing and transforming uploaded assets for usage on the web.

Xata is a Serverless Data platform that simplifies how developers work with data by providing the power of a traditional database with the usability of a SaaS spreadsheet app.

GitHub

Check out the complete source code here.

Netlify

We can access the live demo here.

Prerequisite

Understanding this article requires the following:

  • Installation of Node.js
  • Basic knowledge of JavaScript
  • A Cloudinary account (sign up here)
  • Creating a free account with Xata

Creating a Vue app

Using the vue create <project-name> command, we will create a new Vue app.
The process of scaffolding the project would provide a list of options, which could look like this:

Vue application scaffolding options

We can start our Vue application by running the following command:

cd <project name>
npm run serve
Enter fullscreen mode Exit fullscreen mode

Vue will, in turn, start a hot-reloading development environment that is accessible by default at http://localhost:8080.

Styling

The CSS framework to be used in this project is Tachyons CSS. Install it by running the command below in the terminal.

npm i tachyons
Enter fullscreen mode Exit fullscreen mode

Afterward, make it globally available for usage in the project by adding the line below in our src/main.js:

import 'tachyons/css/tachyons.css';
Enter fullscreen mode Exit fullscreen mode

Creating a Xata Database

To create a database on Xata, we must either log into our account or create a new account. Next, we will go to the user dashboard's database tab to create an appropriate database.

Database creation on Xata

We will create a new table within the database and add the appropriate records/fields that we want the table to have. In this case, we created a user table with three fields: email, password, and username.
These fields signify the data we will be tracking in our application and saving to the database.

Table creation on Xata

Setting up Xata in our Vue application

Installing Xata CLI
We will first need to install Xata CLI on our machine. To do this, we will open the command line terminal as an administrator and run the command below.

npm install -g @xata.io/cli@latest
Enter fullscreen mode Exit fullscreen mode

Installing Xata in our project

To use Xata in our project, we will install the Xata software development kit (SDK) from the command line like so:

npx xata
Enter fullscreen mode Exit fullscreen mode

After this, we can then initialize Xata for use in our application by running the command below:

xata init
Enter fullscreen mode Exit fullscreen mode

Xata will present us varieties of options from which we can choose. In the end, Xata will generate some files for usage, among which we will have the .xatrc and .env files.
We will need to edit the name of the Xata API key in our Vue app to the one below to allow the Vue application to pick up the environment variable.

VUE_APP_XATA_API_KEY="add your api key"
Enter fullscreen mode Exit fullscreen mode

Cloudinary Setup

For our application's asset management, if you haven't already, you need to create an account with Cloudinary by clicking here. Creating an account is entirely free.
We will need to install the Cloudinary Vue.js SDK with the command below before configuring it in your Vue app.

npm install cloudinary-vue
Enter fullscreen mode Exit fullscreen mode

We will need to configure the installed SDK in our Vue app by navigating to the main.js and adding the following to the file.

import Cloudinary from "cloudinary-vue";

Vue.use(Cloudinary, {
  configuration: { 
    cloudName: "XXX," //add the cloudinary cloudname here,
    secure: true }
});
Enter fullscreen mode Exit fullscreen mode

We will upload to Cloudinary the invoicing template we want to use in our application. In this case, we used this Invoicing template from Canva.

Creating the invoice dashboard

The invoice dashboard is where the bulk of the work happens, as this is where we will fill in the data we will add to the invoice and generate the invoice. We will start by creating a file called components/TheDashboardBody.vue, and add a form that allows the user to add the inputs they will show on their invoice.

Creating the basic input data file

Our dashboard page will have a form where users can add the information they want to display on their invoices. While we can manually add all the input fields, we also would like to use a different approach to minimize the number of input fields we will have to add to our file.
To do this, we will create a JSON file called util/basic-form-data.json and add all the data for the basic input fields we want, like so:

[
  {
    "id": 1,
    "inputdata": "",
    "formdata": "Invoice From",
    "type": "text",
    "otherdata": "invoicefrom"
  },
  {
    "id": 2,
    "inputdata": "",
    "formdata": "Invoice To",
    "type": "text",
    "otherdata": "invoiceto"
  },
]
Enter fullscreen mode Exit fullscreen mode

Creating the items input data file

The invoice we are building will also have a section catering to the items or services we rendered. As a result, we will add a JSON file called items-form-data.json and add the things we want. Like so:

[
  {
    "id": 11,
    "inputdata": "",
    "formdata": "Item One",
    "type": "text",
    "otherdata": "itemone"
  },
  {
    "id": 12,
    "inputdata": "",
    "formdata": "Item Two",
    "type": "text",
    "otherdata": "itemtwo"
  },
]
Enter fullscreen mode Exit fullscreen mode

Creating the dashboard input component

We will create an input component with props from the JSON files we created, which will help us create an input field that we will loop over later. To do this, we will create a components/TheInputTemplate.vue file and add the code below:

<template>
  <div>
    <label for="otherData" class="db mb3 black-70 ttu fw7">{{formData}}</label>
    <input
      :value="inputData"
      :type="type"
      id="otherData"
      name="otherData"
      class="db mb3 w-90 br2 pa2 ba bw1 b--black bg-black-05"
      :placeholder="formData"
      @input="$emit('input', $event.target.value)"
    >
  </div>
</template>

<script>
export default {
  props: ["inputData", "formData", "type", "otherData"]
}
</script>
Enter fullscreen mode Exit fullscreen mode

If we match the code above with the items we have in the JSON files, we can see each JSON data's use cases.

Creating the dashboard body

Now, we will pull it together to create a dashboard body by creating a file named components/TheDashboardBody.vue and adding the code below.

<template>
  <div class="flex absolute vh-auto top-4 left-2 right-2 w-90 center mw9">
    <div class="mr3 bg-white w-70 br3 pa3">
      <form @submit.prevent="generateInvoice" class="flex justify-between flex-wrap">
        <div v-for="basicinfo in basicData" :key="basicinfo.id" class="w-50-l w-100">
          <TheDashboardInput v-model="basicinfo.inputdata" :formData="basicinfo.formdata" :type="basicinfo.type" :otherData="basicinfo.otherdata"/>
        </div>

        <h3 class="dark-blue w-100 f3 underline">ITEMS</h3>
        <div v-for="item in itemsData" :key="item.id" class="w-50-l w-100">
          <TheDashboardInput v-model="item.inputdata" :formData="item.formdata" :type="item.type" :otherData="item.otherdata"/>
        </div>

        <div class="w-100">
          <button type="submit" class="f6 ttu tracked black-80 bg-black pa3 br3 white bb link b--black hover-white hover-bg-dark-blue bg-animate pointer">
            Create Invoice
          </button>
        </div>
      </form>
    </div>
    <div class="bg-white w-30 br3 pa3">
      <iframe :src="iframeSRC ? iframeSRC : initialSRC" width="100%" height="100%" />
    </div>
  </div>
</template>

<script>
import TheDashboardInput from "@/components/TheDashboardInput.vue";
import itemsData from "../../util/items-form-data.json";
import basicData from "../../util/basic-form-data.json"
export default {
  components: {
  TheDashboardInput
  },
  data: ()=>({
  basicData,
  itemsData,
  basicDataInfo: {},
  itemInfo: {},
  iframeSRC: '',
  initialSRC: 'https://res.cloudinary.com/moerayo/image/upload/v1668849124/invoiced.pdf',
  }),
  methods: {
    generateBasicDataInfo() {
      let newBasicData = basicData.map(({otherdata,inputdata })=> ({[otherdata]:inputdata}))
      this.basicDataInfo = Object.assign({}, ...newBasicData)
    },
    generateItemInfo(){
      let newItemData = itemsData.map(({otherdata,inputdata })=> ({[otherdata]:inputdata}))
      this.itemInfo = Object.assign({}, ...newItemData)
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

From the code block above, we achieved the following:

  • We imported the two JSON files we created - itemsData and basicData - and added both to the data options of the Vue script tag to use later in our code
  • Looped through each of the JSON data we instantiated in the data options using the Vue v-for directive
  • Imported the TheDashboardInput component we created and passed in the suitable data to fill the props option
  • Added an iframe that shows the current invoice template and the yet to be generated filled invoice template
  • Created a generateBasicDataInfo() and generateItemInfo() methods that both take in their respective data from the JSON files. These data are reformatted using the JavaScript map() function to create a new array with the otherdata as the array key and the inputdata as the value. The result of this formatted array is then copied into an object using the JavaScipt Object.assign() method. Doing this is crucial for picking the data passed through the v-model

Creating the invoice template

To generate our invoice template - i.e., what we will download and send to the client - we will use Cloudinary's powerful image and text transformation features. These features will enable us to overlay the invoice template with texts. To do this, we will create a file called components/TheInvoiceTemplate.vue and add the code below like so:

<template>
  <div>
    <cld-image publicId="invoiced.pdf" ref="ref" >
      <cld-transformation flags="rasterize" />
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 60, text: basicDataInfo.invoicefrom,  fontWeight: 'bold'}" color="#023982" gravity="west" x="130" y="-670"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 45, text: basicDataInfo.invoiceto}" color="#023982" gravity="west" x="130" y="-480"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.date}" color="#000" gravity="west" x="560" y="-525"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.invoicenumber}" color="#000" gravity="west" x="610" y="-465"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 43, text: basicDataInfo.total}" color="#023982" gravity="west" x="990" y="-475"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.bankname}" color="#5b4f47" gravity="west" x="265" y="255"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.accountnumber}" color="#5b4f47" gravity="west" x="265" y="285"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 30, text: basicDataInfo.subtotal}" color="#000" gravity="west" x="1000" y="180"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 30, text: basicDataInfo.tax}" color="#000" gravity="west" x="1000" y="230"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 43, text: basicDataInfo.total}" color="#000" gravity="west" x="1000" y="300"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemone}" color="#000" gravity="west" x="155" y="-248"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemtwo}" color="#000" gravity="west" x="155" y="-180"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemthree}" color="#000" gravity="west" x="155" y="-80"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemfour}" color="#000" gravity="west" x="155" y="-1"/>
    </cld-image>  
  </div>
</template>
<script>
export default {
  name: "TheResumeTemplate",
  props: {
    basicDataInfo: {
      type: Object
    },
    itemInfo: {
      type: Object
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

From the above, we achieved the following:

  • Cloudinary provides us with several templates to use in our Vue app. One of which is the <cld-image /> and the <cld-transformation /> components we will use to generate our invoice
  • Passed in the publicId of the Cloudinary file we will be using, which is named invoiced.pdf
  • Configured the Cloudinary components to accept the basicDataInfo and itemInfo props which we will pass in from the TheDashboardbody component.
  • Cloudinary components support asset transformations, which we leveraged by adding text transformations like the font family, colors, font sizes, and the specific places we want the texts to be placed on the invoice.

To render this template, we will need to import it in the TheDashboadBody component like so:


<template>
<div class="dn" v-if="showTemplate">
    <TheInvoiceTemplate :basicDataInfo="basicDataInfo" :itemInfo="itemInfo" ref="ref" />
  </div>
</template>

<script>
import TheInvoiceTemplate from "@/components/TheInvoiceTemplate.vue";
export default {
  components: {
  TheInvoiceTemplate,
  },
  data: ()=>({
  showTemplate: false,
  })
}
</script>
Enter fullscreen mode Exit fullscreen mode

We can find the complete code for the TheDashboardBody.vue in this GitHub gist.
At this point, the UI for TheDashboardBody.vue file should look like the below:

The dashboard body interface

Creating the Signup Page

We will need a user to sign up for our application to create an invoice. To do this, we will create a file named views/SignupView.vue and add the following code to it:


<template>
  <div class="bg-lightest-blue vh-100">
    <div class="pv5 ph2">
      <form class="ba b--dark-blue bw3 bg-white br2 mw6 w-40-m w-70 w-20-l center pa3 shadow-5" @submit.prevent="signUp">
        <h2 class="ttc tc">
          Sign up
        </h2>
        <label for="name" class="db mb2 black-70">Name</label>
        <input
          id="name"
          v-model="username"
          name="name"
          type="text"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1 b--lightest-blue"
          placeholder="John Doe"
        >
        <label for="email" class="db mb2 black-70">Email</label>
        <input
          id="email"
          v-model="email"
          name="email"
          type="email"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1 b--lightest-blue"
          placeholder="example@email.com"
        >
        <label for="password" class="db mb2 black-70">Password</label>
        <input
          id="password"
          v-model="password"
          name="password"
          type="password"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1 b--lightest-blue"
          placeholder="••••••••"
        >
        <button type="submit" class="center db pa3 mb3 tracked bg-dark-blue ba br3 white pointer hover-black hover-bg-lightest-blue bg-animate pointer">
          Sign up
        </button>
        <p>Already have an account? <a href="/signin" class="black-70 b">Sign in</a> </p>
      </form>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

By this, we have successfully created an interface for our Signup page. Now we will add the functionality for authentication and authorization.

We will use Xata to accomplish our authentication goal. To do this, we will add this to the SignupView.view file:

<script>
import { getXataClient } from '@/xata'
export default {
  name: 'signup',
  data: () => ({
    username: '',
    email: '',
    password: '',
  }),
  methods: {
    async signUp() {
      const xata = getXataClient()
      const user = await xata.db.users.filter('username', this.username).getFirst()
      if (!user) {
        await xata.db.users.create({
          username: this.username,
          password: this.password,
          email: this.email
        }).then((res) => {
          this.$router.push({path:`/dashboard/${res.username}`, params: user})
        })
        this.$notify({type: 'success', text: "Account creation successful!" })
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

From the code block above, we achieved the following:

  • Imported the Xata client
  • Checked if a user exists and created a new user if the current user does not exist
  • At successful account creation, the user's view gets changed to the dashboard from where they can generate an invoice.

At this point, our application should look like the below:

signup interface

Creating the SignIn page

We also need to create options to allow signing in for people who have already created an account. To do this, we will create a file called SigninView.vue and add the code below:


<template>
  <div class="bg-lightest-blue vh-100">
    <div class="pv5 ph2">
      <form class="ba b--dark-blue bw3 bg-white br2 mw6 w-40-m w-70 w-20-l center pa3 shadow-5" @submit.prevent="signIn">
        <h2 class="ttc tc">
          Sign In
        </h2>
        <label for="email" class="db mb2 black-70">Email</label>
        <input
          id="email"
          v-model="email"
          name="email"
          type="email"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1  b--lightest-blue"
          placeholder="example@email.com"
        >
        <label for="password" class="db mb2 black-70">Password</label>
        <input
          id="password"
          v-model="password"
          name="password"
          type="password"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1  b--lightest-blue"
          placeholder="••••••••"
        >
        <button type="submit" class="center db pa3 mb3 tracked bg-dark-blue ba br3 white pointer hover-black hover-bg-lightest-blue bg-animate pointer">
          Sign in
        </button>
      </form>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The above creates a sign-in interface for us. But we will need to wrap some functionalities around it to make it work as expected.
To do this, we will add the code below in the SigninView.vue.


<script>
import { getXataClient } from '@/xata'
export default {
  data: () => ({
    email: '',
    password: ''
  }),
  methods: {
    async signIn() {
      const xata = getXataClient()
      const user = await xata.db.users.filter('email', this.email).getFirst()
      if (!this.email || !this.password ){
        this.$notify({type: 'error', text: "Please fill all empty fields"})
      } else if (this.email !== user.email || this.password !== user.password){
        this.$notify({type: 'error', text: "Incorrect credentials"})
        this.email = '';
        this.password = '';
      } else {
        this.$router.push({path:`/dashboard/${user.username}`, params: user})
        this.$notify({type: 'success', text: "Login successful!"})
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

From the above, we created a function called signIn function that does the following:

  • Imported the Xata client
  • Checks if the user passes an email and password, then returns the appropriate error if it is unavailable
  • Cross checks the email and password entered to be sure it matches the email and password from the database
  • If the checks pass, the page gets routed to the dashboard, where the user can generate n invoice

At this point, our interface will look like this:

Signin interface

At this point, our complete application will look like the below:

Complete application interface

Conclusion

This article discusses generating an invoice using Cloudinary and Xata. Cloudinary was used for asset management and text transformation, while we used Xata for authentication.

Resources

Latest comments (2)

Collapse
 
lalami profile image
Salah Eddine Lalami

Thanks for sharing, @ IDURAR , we use are using node.js react.js & redux

Here Tutorial about : 🚀 Building and Generate Invoice PDF with React.js , Redux and Node.js : dev.to/idurar/building-an-invoice-...

 Building and Generate Invoice PDF with React.js , Redux and Node.js

Collapse
 
jorjadonnell profile image
JorjaDonnell

Your content is really helpful information! totka to get lost love back