DEV Community

Cover image for How to Build an Income Tracker Using Vue.js and Appwrite
teri
teri

Posted on • Updated on

How to Build an Income Tracker Using Vue.js and Appwrite

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.

vue create income-tracker
Enter fullscreen mode Exit fullscreen mode

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

cd income-tracker 

yarn serve
Enter fullscreen mode Exit fullscreen mode

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.

yarn add appwrite
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

attributes

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.

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

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

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

collection id

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.

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

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.

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

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.

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

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.

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

Our app should look like this with the recent changes.

income tracker app

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.

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

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.

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

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.

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

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

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.

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

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:

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

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.

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

The image below represents the working app.

working demo

Top comments (3)

Collapse
 
maxwizard01 profile image
maxwizard01

Your codes is not in the code tag

Collapse
 
terieyenike profile image
teri

It took me some time to find the fault in the codeblock. It is fixed now.

Collapse
 
terieyenike profile image
teri

I don't know why Dev formatted that way. I have tried to fix it but it shows in a different format.
I will continue to check on the code block and make sure it works.