Create Expense Tracker Vue Application

Vue 23, Oct 2023

In this post, we will design "Expense Tracker" application using Tailwind CSS.

Install Tailwind CSS and Configure

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Update tailwind.config.js using below code.

module.exports = {
  content: [
    "./resources/**/*.blade.php",
    "./resources/**/*.js",
    "./resources/**/*.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Now, add below code in resources/css/app.css

@tailwind base;
@tailwind components;
@tailwind utilities

body {
    font-family: "Roboto", sans-serif;
}

@layer components {
    .btn {
        @apply font-medium rounded-full text-sm px-5 py-2.5 text-center mr-2 mb-2;
    }

    .btn-primary {
        @apply text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300  dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800;
    }

    .form-label {
        @apply block mb-2 text-sm font-medium text-gray-900 dark:text-white
        block mb-2 text-sm font-medium text-gray-900 dark:text-white;
    }

    .form-control {
        @apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500;
    }
}

Create ExpenseItem.vue file inside shared/components folder

<template lang="">
    <div>
        <div class="relative w-full p-5 bg-white rounded-lg shadow">
            <h2 class="font-bold text-xl">Rs. {{ item.amount }}</h2>
            <div class="flex flex-col mt-4 gap-2">
                <p class="text-gray-500 text-sm font-medium">{{ item.date }}</p>
                <p>{{ item.description }}</p>
            </div>
            <div class="absolute gap-2 right-2 top-2">
                <router-link
                    class="font-medium text-blue-600 dark:text-blue-500 hover:underline mr-2"
                    :to="{
                        name: 'EditExpens',
                        params: { id: item.id },
                    }"
                    >Edit</router-link
                >
                <button
                    @click="onDeleteClick(item.id)"
                    class="font-medium text-red-600 dark:text-red-500 hover:underline mr-2"
                >
                    Delete
                </button>
            </div>
        </div>
    </div>
</template>
<script>
import { ref } from "vue";

export default {
    name: "ExpensItem",
    props: {
        item: Object,
    },
    emits: ["onDeleteClick"],
    setup(props, context) {
        const onDeleteClick = (id) => {
            context.emit("onDeleteClick", id);
        };

        return {
            onDeleteClick,
        };
    },
};
</script>

Now, create three files inside vue/pages/expense folder

  1. ListExpens.vue
  2. CreateExpens.vue
  3. EditExpens.vue

ListExpens.vue

<template lang="">
    <div class="mt-6">
        <div class="container">
            <div class="flex justify-between mb-4">
                <h4 class="text-xl font-extrabold">Expenses</h4>
                <router-link class="btn btn-primary" to="/expens/create"
                    >Add Expens</router-link
                >
            </div>

            <div class="relative flex flex-col gap-3">
                <expens-item
                    v-for="item in expenses"
                    :item="item"
                    :key="item.id"
                    @onDeleteClick="deleteExpens($event)"
                ></expens-item>
            </div>
        </div>
    </div>
</template>
<script>
import { ref, onMounted } from "vue";
import { AllExpenses, DeleteExpenses } from "@/core/http/api.js";
import { createToaster } from "@meforma/vue-toaster";
import moment from "moment";
import ExpensItem from "@/shared/components/ExpensItem.vue";

export default {
    name: "ListExpens",
    components: {
        "expens-item": ExpensItem,
    },
    setup() {
        const expenses = ref([]);
        const toaster = createToaster({
            position: "top",
        });

        const dateTime = (value) => {
            return moment(value).format("ll");
        };

        const getAllExpenses = async () => {
            const { data } = await AllExpenses();
            expenses.value = data.data;
        };

        const deleteExpens = async (id) => {
            const { data } = await DeleteExpenses(id);
            toaster.success(data.message);
            getAllExpenses();
        };

        onMounted(() => {
            getAllExpenses();
        });

        return {
            expenses,
            getAllExpenses,
            dateTime,
            deleteExpens,
        };
    },
};
</script>

CreateExpens.vue

<template lang="">
    <div class="mt-6">
        <div class="container">
            <div class="flex justify-between mb-4">
                <h4 class="text-xl font-extrabold">Add Expens</h4>
            </div>
            <div class="relative overflow-x-auto">
                <div class="w-1/2">
                    <a
                        href="#"
                        class="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
                    >
                        <form @submit.prevent="addExpens">
                            <div class="mb-6">
                                <label for="amount" class="form-label"
                                    >Amount</label
                                >
                                <input
                                    type="text"
                                    id="amount"
                                    class="form-control"
                                    placeholder="Amount"
                                    v-model="expens.amount"
                                    autocomplete="off"
                                    required
                                />
                            </div>
                            <div class="mb-6">
                                <label for="description" class="form-label"
                                    >Description</label
                                >
                                <input
                                    type="text"
                                    id="description"
                                    class="form-control"
                                    placeholder="Description"
                                    v-model="expens.description"
                                    required
                                />
                            </div>
                            <div class="mb-6">
                                <label for="date" class="form-label"
                                    >Description</label
                                >
                                <input
                                    type="date"
                                    id="date"
                                    class="form-control"
                                    placeholder="date"
                                    v-model="expens.date"
                                    required
                                />
                            </div>
                            <button
                                type="submit"
                                class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
                            >
                                Submit
                            </button>
                        </form>
                    </a>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import { ref, onMounted } from "vue";
import { CreateExpens } from "@/core/http/api.js";
import router from "@/routes";
import { createToaster } from "@meforma/vue-toaster";
export default {
    name: "CreateExpens",
    setup() {
        const toaster = createToaster({
            position: "top",
        });
        const expens = ref({ amount: "", description: "", date: "" });

        const addExpens = async () => {
            console.log(expens.value);
            const { data } = await CreateExpens(expens.value);
            toaster.success(data.message);
            router.push({ name: "ListExpens" });
        };

        return { expens, addExpens };
    },
};
</script>

EditExpens.vue

<template lang="">
    <div class="mt-6">
        <div class="container">
            <div class="flex justify-between mb-4">
                <h4 class="text-xl font-extrabold">Edit Expens</h4>
            </div>
            <div class="relative overflow-x-auto">
                <div class="w-1/2">
                    <a
                        href="#"
                        class="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
                    >
                        <form @submit.prevent="updateExpens">
                            <div class="mb-6">
                                <label for="amount" class="form-label"
                                    >Amount</label
                                >
                                <input
                                    type="text"
                                    id="amount"
                                    class="form-control"
                                    placeholder="Amount"
                                    v-model="expens.amount"
                                    autocomplete="off"
                                    required
                                />
                            </div>
                            <div class="mb-6">
                                <label for="description" class="form-label"
                                    >Description</label
                                >
                                <input
                                    type="text"
                                    id="description"
                                    class="form-control"
                                    placeholder="Description"
                                    v-model="expens.description"
                                    required
                                />
                            </div>
                            <div class="mb-6">
                                <label for="date" class="form-label"
                                    >Description</label
                                >
                                <input
                                    type="date"
                                    id="date"
                                    class="form-control"
                                    placeholder="date"
                                    v-model="expens.date"
                                    required
                                />
                            </div>
                            <button
                                type="submit"
                                class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
                            >
                                Submit
                            </button>
                        </form>
                    </a>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import { ref, onMounted } from "vue";
import { UpdateExpens, GetExpenses } from "@/core/http/api.js";
import { useRoute } from "vue-router";
import router from "@/routes";
import { createToaster } from "@meforma/vue-toaster";
export default {
    name: "EditExpens",
    setup() {
        const route = useRoute();
        const toaster = createToaster({
            position: "top",
        });
        const expens = ref({ amount: "", description: "", date: "" });

        const updateExpens = async () => {
            const { data } = await UpdateExpens(expens.value, route.params.id);
            toaster.success(data.message);
            router.push({ name: "ListExpens" });
        };

        const getExpens = async (id) => {
            const { data } = await GetExpenses(id);
            expens.value = data.data;
        };

        onMounted(() => {
            getExpens(route.params.id);
        });

        return { expens, updateExpens, getExpens };
    },
};
</script>

Install router package using below command:

npm install vue-router@4

Now, create routing file inside vue/routes/index.js folder

import { createRouter, createWebHistory } from "vue-router";
import ListExpens from "@/pages/expens/ListExpens.vue";
import CreateExpens from "@/pages/expens/CreateExpens.vue";
import EditExpens from "@/pages/expens/EditExpens.vue";

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            path: "/",
            name: "ListExpens",
            component: ListExpens,
        },
        {
            path: "/expens/create",
            name: "CreateExpens",
            component: CreateExpens,
        },
        {
            path: "/expens/edit/:id",
            name: "EditExpens",
            component: EditExpens,
        },
    ],
});

export default router;

Now, create api.js file inside vue/core/http folder

import axios from "axios";

const api = axios.create({
    baseURL: "/api/",
});

// Expens
const expensApiUrl = "/expens";
export const AllExpenses = () => api.get(expensApiUrl);
export const GetExpenses = (id) => api.get(`${expensApiUrl}/${id}`);
export const CreateExpens = (expens) => api.post(expensApiUrl, expens);
export const UpdateExpens = (expens, id) =>
    api.put(`${expensApiUrl}/${id}`, expens);
export const DeleteExpenses = (id) => api.delete(`${expensApiUrl}/${id}`);

Now, update App.vue file with below code

<template lang="">
    <div>
        <Navbar />
        <router-view></router-view>
    </div>
</template>
<script>
import Navbar from "@/shared/components/Navbar.vue";
export default {
    name: "App",
    components: {
        Navbar,
    },
    setup() {},
};
</script>

Now, add vue routes inside resources/js/app.js file

import "./bootstrap";
import { createApp } from "vue";
import App from "../vue/App.vue";
import router from "../vue/routes";
import Toaster from "@meforma/vue-toaster";

const app = createApp(App);
app.use(Toaster);
app.use(router);
app.mount("#app");

Now, run project using below commands:

php artisan serve
npm run dev