
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
- ListExpens.vue
- CreateExpens.vue
- 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