How to Build an Income Tracker Using Vue.js and Appwrite
Tags: appwritehack, vue, tutorial, opensource
Published: 2022-05-12 23:59:13

An income tracker will allow users to monitor and keep track of their expenses. The income tracker app makes it easy for anyone to add, edit, update, and delete specific data from the client-side, and it updates accordingly in the database.

This article will teach us how to build an income tracker app with Vue.js and using Appwrite for storing the data.

First, let's get an introduction to some of the technologies used to build the income tracker app.

**Vue.js**: It is an open-source progressive and versatile frontend framework for building web user interfaces and single page applications with the bedrock technologies of HTML, CSS, and JavaScript.

**Appwrite**: It is a secure self hosted open-source backend-as-a-service that provides developers all the core APIs to build applications ranging from web to mobile.

## Getting Started with Vue
In our terminal run the following command. This will create a boilerplate app and scaffold the Vue.js code for developmemt.

```bash
vue create income-tracker
```

With the project set up, let's start a development server that is accessible on `http://localhost:8080`

```bash
cd income-tracker 

yarn serve
```

In the terminal, let's install Appwrite client-side SDK with the command. The installation of this dependency will enable configure Appwrite and use it across our application.

```bash
yarn add appwrite
```

## Appwrite Setup
To get the full functionalities of Appwrite backend features, we will manually set it up using Docker.

Now, let's get the Appwrite server running. Before, we can get this to work, install the [Docker CLI](https://www.docker.com/products/docker-desktop). In our project folder, install the Docker installer tool in the terminal which will give us access to our Appwrite console dashboard. The installation supports different operating system (OS) with this [getting started guide](https://appwrite.io/docs/installation).

Note: Use `http://localhost/console` to access the Appwrite console.

## Creating a New Appwrite Project
Once we have created an account, click on the Create Project. We will name the project `income-tracker`.

![Add new project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94xn3jql6um8w0yogdav.png)


With the income tracker project created, let's create a collection and add a lists of attributes.

Navigate to the `Database` tab and click the `Add Collection` and create a new collection called `tracker`


![collection name - tracker](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5c77or4yh352gzty7m2.png)

Within the `collection`, click the `Attributes` tab and create the following attributes for our document.


![attributes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9uepwwdphltpv2cqvao3.png)

The most exciting part of this configuration is that Appwrite will accept the data from the client-side and store them in the documents.

## Initialising the Web SDK
In the project with our Vue code, create a new file `utils.js` in the `src` directory to define the new Appwrite instance and other helpful variables.

Copy and paste the following code.

```JavaScript
import { Appwrite } from 'appwrite';
// Init your Web SDK
const appwrite = new Appwrite();
appwrite
  .setEndpoint('http://EndpointURL.example') // Replace this with your endpoint
  .setProject('ProjectID'); // Replace this with your ProjectID

appwrite.account.createAnonymousSession().then(
  (response) => {
    console.log(response);
  },
  (error) => {
    console.log(error);
  }
);

export const db = appwrite.database;
export const COLLECTION_ID = 'COLLECTION ID'; // Replace with your Collection ID
```

To bypass some security requirements, we created an anonymous session on Appwrite.

The `PROJECT_ID` in the above code, the value is found in the `Settings` under the `Home` tab.

![project id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1tair3m5l1za8w7wry2g.png)

For the `COLLECTION_ID`, access it in the `Collection Settings` in the `Database` tab.

![collection id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h8yrydee4zrqj0zumzx8.png)

At the `Collection Level` within the settings tab, set the Read and Write access to have the values of `role:all`.

## Creating the Income Tracker
Let's create the navigation menu that will display the current expenses. 

In the `Header.vue` file in the components folder, paste in the following code.

```javascript
<template>
  <header>
    <h1>Income Tracker</h1>
    <div class="total-income">
      $500
    </div>
  </header>
</template>

<style scoped>
header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

h1, .total-income {
  color: var(--grey);
  font-weight: 700;
  font-family: 'Inter', sans-serif;
}

h1 {
  font-size: 2rem;
}

.total-income {
  font-size: 1.75rem;
  background: var(--bg-total-income);
  padding: .3125rem 1.5625rem;
  border-radius: 0.5rem;
}
</style>
```

## Creating the Income Form
Here, we will create the income form with input that accept text and date attributes. 

Create another file in the components folder called `IncomeForm.vue` and paste the following code.

```javascript
<template>
  <section>
    <form class="income-form">
      <div class="form-inner">
        <input
          v-model="income"
          placeholder="Income Description"
          type="text"
          required
        />
        <input
          v-model="price"
          min="0"
          placeholder="Price..."
          type="number"
          required
        />
        <input
          v-model="date"
          placeholder="Income date..."
          type="date"
          required
        />
        <input type="submit" value="Add Income" />
      </div>
    </form>
  </section>
</template>

<script>
export default {
  data() {
    return {
      income: '',
      price: '',
      date: null,
    };
  },
};
</script>
```

The code above has the data properties for the income, price, and date variables set to an empty string and null respectively. For the reference of this data properties, we bound them to the `<input>` element using the `v-model` directive.

Another important component that we need for this application is a list that will hold all the accepted data.

Create the `IncomeList.vue` component and paste the following code.

```javascript
<template>
  <section>
    <div class="income-item">
      <div class="space desc">Web Design</div>
      <div class="space price">$500</div>
      <div class="space date">2022-05-24</div>
      <div class="btn">
        <div class="btn-edit">update</div>
        <div class="btn-del">delete</div>
      </div>
    </div>
  </section>
</template>

<style scoped>
section {
  padding: unset;
}

.income-item {
  background: #ffffff;
  padding: 0.625em 0.94em;
  border-radius: 5px;
  box-shadow: 0px 4px 3px rgba(0, 0, 0, 0.1);
  position: relative;
  margin: 2em 0;
}

.space + .space {
  margin-top: 1em;
}

.desc {
  font-size: 1.5rem;
}

.btn {
  position: absolute;
  bottom: 0;
  right: 0;
  display: flex;
  align-items: center;
  padding: 0.75em;
  text-transform: capitalize;
}

.btn-edit {
  color: var(--grey);
}

.btn-del {
  margin-left: 10px;
  color: var(--alert);
}

.btn-edit,
.btn-del {
  cursor: pointer;
}

@media screen and (min-width: 768px) {
  .desc {
    font-size: 2rem;
  }

  .price {
    font-size: 1.5rem;
  }

  .date {
    font-size: 1.5rem;
  }

  .btn-edit,
  .btn-del {
    font-size: 1.5rem;
  }
}

@media screen and (min-width: 1200px) {
  .income-item,
  .modal__wrapper {
    width: 80%;
    margin-inline: auto;
  }
}
</style>
```

With this in place, let's import the `IncomeForm.vue`, `IncomeList.vue`, and `Header.vue` component into the application entry point `App.vue` with the following code.

```javascript
<template>
  <section class="container">
    <Header />
    <IncomeForm />
    <div>
      <IncomeList />
    </div>
  </section>
</template>

<script>
import Header from "./components/Header"
import IncomeForm from './components/IncomeForm'
import IncomeList from "./components/IncomeList";

export default {
  name: 'App',
  components: {
    Header,
    IncomeForm,
    IncomeList
  },
}
</script>

<style>
:root {
  --light: #F8F8F8;
  --dark: #313131;
  --grey: #888;
  --primary: #FFCE00;
  --secondary: #FE4880;
  --alert: #FF1E2D;
  --bg-total-income: #DFDFDF;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Reset margins */
body,
h1,
h2,
h3,
h4,
h5,
p,
figure,
picture {
  margin: 0;
}

body {
  font-family: 'Montserrat', sans-serif;
  background: var(--light)
}

h1,
h2,
h3,
h4,
h5,
h6,
p {
  font-weight: 400;
}

img,
picutre {
  max-width: 100%;
  display: block;
}

/* make form elements easier to work with */
input,
button,
textarea,
select {
  font: inherit;
}

button {
  cursor: pointer;
}

section {
  padding: 3em 0;
}

.container {
  max-width: 75rem;
  width: 85%;
  margin-inline: auto;
}

/*income form and income list styling*/
input {
  width: 100%;
  border: 1px solid gray;
}

.income-form {
  display: block;
}

.form-inner input {
  font-size: 1.125rem;
  padding: 0.625em 0.94em;
  background: #fff;
  border-radius: 5px;
}

input + input {
  margin-top: 2em;
}

.form-inner input[type=submit] {
  cursor: pointer;
  background-image: linear-gradient(to right, var(--primary) 50%, var(--primary) 50%, var(--secondary));
  background-size: 200%;
  background-position: 0%;
  color: var(--dark);
  text-transform: uppercase;
  transition: 0.4s;
  border: unset;
}

.form-inner input[type="submit"]:hover {
  background-position: 100%;
  color: #FFF;
}

@media screen and (min-width: 1200px) {
  .form-inner {
    display: flex;
    justify-content: center;
  }

  input + input {
    margin: 0;
  }

  input {
    border: unset;
  }

}
</style>
```

Our app should look like this with the recent changes.

![income tracker app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ddujk1fesbappqe77n6.png)

## Fetch All Income List
We create a function to fetch all the listed income from the Appwrite database when the page loads. Update the `<script>` section in the `App.vue` file with the following code.

```javascript
<script>
// imported component

import { COLLECTION_ID, db } from '@/utils';

export default {
  name: 'App',
  components: {
    // all components
  },
  created() {
    this.fetchLists();
  },
  data() {
    return {
      lists: [],
    };
  },
  methods: {
    fetchLists() {
      let promise = db.listDocuments(COLLECTION_ID);

      promise.then(
        (res) => {
          this.lists = res.documents;
        },
        (err) => {
          console.log(err);
        }
      );
    },
  },
};
</script>
```

We created a lists array property in the `data` function to store the lists and retrieve them using the `listDocuments` API.

In the `created()` lifecycle method, run the `fetchLists()` function when the App component is created.

Finally update the `<template>` section in the `App.vue` component with the following code.

```javascript
<template>
  <section class="container">
    <Header :totalIncome="lists" />
    <IncomeForm :fetchLists="fetchLists" />
    <div v-for="data in lists" :key="data.income">
      <IncomeList :data="data" v-on:refreshData="fetchLists" />
    </div>
  </section>
</template>
```

To reuse the function to fetch all lists after creating a new income list, we bind the `:fetchLists` prop to the `fetchLists` method we defined earlier.


## Creating a new Income List
In the `IncomeForm.vue` file is where we handle the income addition to the database.

Paste the following code to update the file.

```javascript
<template>
  <section>
    <form class="income-form" @submit.prevent="addIncome">
      <div class="form-inner">
        <input
          v-model="income"
          placeholder="Income Description"
          type="text"
          required
        />
        <input
          v-model="price"
          min="0"
          placeholder="Price..."
          type="number"
          required
        />
        <input
          v-model="date"
          placeholder="Income date..."
          type="date"
          required
        />
        <input type="submit" value="Add Income" />
      </div>
    </form>
  </section>
</template>

<script>
import { COLLECTION_ID, db } from '@/utils';

export default {
  props: ['fetchLists'],
  // data
  methods: {
    addIncome() {
      if (this.income === '' && this.price === '' && this.date === '') {
        return;
      }

      let promise = db.createDocument(COLLECTION_ID, 'unique()', {
        income: this.income.charAt(0).toUpperCase() + this.income.slice(1),
        price: this.price,
        date: this.date,
      });

      promise.then(
        () => {
          this.fetchLists();
          this.income = '';
          this.price = '';
          this.date = '';
        },
        (err) => {
          console.log(err);
        }
      );
    },
  },
};
</script>
```

In the `addIncome` method, we use Appwrite’s `createDocument` API to write a new list to the database. An error message is logged if the write operation fails. We fetch an updated list of all income after adding a new list.

The Appwrite web console will display one document representing a list in the image below:


![list in the document](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oguupvb1x7tiak4pnofz.png)

## Updating the Income list Component
In the `App.vue` component, we update the income list component’s props to include the looped `data` and the `fetchLists` method.

```javascript
<template>
  <section class="container">
    <Header :totalIncome="lists" />
    <IncomeForm :fetchLists="fetchLists" />
    <div v-for="data in lists" :key="data.income">
      <IncomeList :data="data" v-on:refreshData="fetchLists" />
    </div>
  </section>
</template>

<script>
// import component
import IncomeList from './components/IncomeList';

export default {
  components: {
    // other components
    IncomeList,
  },
};
</script>
```

fetchLists runs once the refreshData event is fired.

Let's update the `IncomeList.vue` component to handle list updates and deletion. We will also include a component to edit an income list. First, we add the update list function in the script portion with:

```javascript
<script>
import { db } from '@/utils';

export default {
  props: ['data'],
  data() {
    return {
      open: false,
      income: '',
      price: '',
      date: '',
    };
  },
  methods: {
    updateIncome() {
      this.open = !this.open;
    },

    updateIncomeMethod() {
      if (this.income === '' && this.price === '' && this.date === '') {
        return;
      }

      let promise = db.updateDocument(this.data.$collection, this.data.$id, {
        income: this.income.charAt(0).toUpperCase() + this.income.slice(1),
        price: this.price,
        date: this.date,
      });
      this.open = false;
      promise.then(
        () => {
          this.$emit('refreshData');
        },
        (err) => {
          console.log(err);
        }
      );
    },

    deleteIncome() {
      let promise = db.deleteDocument(this.data.$collection, this.data.$id);
      promise.then(
        () => {
          this.$emit('refreshData');
        },
        (err) => {
          console.log('error occured', err);
        }
      );
    },
  },
};
</script>
```

We added a state variable to manage the visibility of a list’s action buttons. Appwrite’s updateDocument API uses the collection ID and document ID passed as props to update the comment. Once the list is updated, we emit the refreshData event to fetch all income list.

We update the template portion to utilize the methods and variables created.

```javascript
<template>
  <section>
    <div class="income-item">
      <div class="space desc">{{ data.income }}</div>
      <div class="space price">${{ data.price.toLocaleString('en-US') }}</div>
      <div class="space date">{{ data.date }}</div>
      <div class="btn">
        <div class="btn-edit" @click.prevent="updateIncome">update</div>
        <div class="btn-del" @click.prevent="deleteIncome">delete</div>
      </div>
    </div>

    <div v-if="this.open" class="modal__wrapper">
      <form class="income-form" @submit.prevent="updateIncomeMethod">
        <div class="form-inner">
          <input
            v-model="income"
            :placeholder="data.income"
            type="text"
            required
          />
          <input
            v-model="price"
            :placeholder="data.price"
            min="0"
            type="number"
            required
          />
          <input v-model="date" :placeholder="data.date" type="date" required />

          <input type="submit" value="Update" />
        </div>
      </form>
    </div>
  </section>
</template>
```

The image below represents the working app.


![working demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lzdwy21al09dcaqwswcj.png)