YouTube Courses - Learn Smarter

YouTube-Courses.site transforms YouTube videos and playlists into structured courses, making learning more efficient and organized.

Vue.js 3 and the Composition API: Building an Expense Tracker - An Educational Guide



This chapter will guide you through the process of building a simple expense tracker application using Vue.js 3, specifically leveraging the Composition API and the script setup syntax. This project, originally part of a “20 Vanilla JavaScript Projects” course and later adapted for React, serves as an excellent practical example for understanding modern Vue.js development.

1. Introduction to Vue.js 3 and the Composition API

Vue.js is a progressive JavaScript framework for building user interfaces. Known for its ease of use and flexibility, Vue.js has become a popular choice for front-end development. Version 3 of Vue.js introduced significant improvements, most notably the Composition API.

Vue.js: A progressive JavaScript framework used for building user interfaces and single-page applications. It is designed to be incrementally adoptable and focuses on the view layer.

Traditionally, Vue.js components were structured using the Options API.

Options API: The original way to structure Vue.js components, where component logic is organized into options like data, methods, computed, and watch. This approach groups related logic by option type.

However, Vue.js 3 emphasizes the Composition API as a more organized and efficient way to manage component logic.

1.1 Understanding the Composition API

The Composition API is a set of APIs that allows you to author Vue.js components using imported functions instead of declaring options. It aims to improve code reusability, readability, and especially reactivity.

Reactivity: In front-end frameworks, reactivity refers to the automatic updating of the user interface (UI) when the underlying data changes. This eliminates the need for manual DOM manipulation.

Key Benefits of the Composition API:

  • Improved Logic Organization: The Composition API allows you to group related logic together, regardless of the option type. This leads to more maintainable and understandable code, especially in complex components.
  • Enhanced Reusability: Logic can be extracted into reusable functions (composables), promoting code sharing across components.
  • Better Readability: Code becomes more linear and easier to follow as logic is organized by feature rather than option.
  • Simplified Reactivity: The Composition API provides straightforward ways to establish reactivity, making UI updates more predictable and efficient.

The Composition API contrasts with the Options API by moving away from organizing code by options (like data, methods, computed) and instead focusing on organizing code by logical features. This approach often results in cleaner and more scalable component structures.

1.2 Project Overview: Expense Tracker Application

This project involves building a web application to track income and expenses. The application will feature:

  • Total Balance Display: Shows the overall financial balance.
  • Income and Expense Balances: Displays separate balances for total income and total expenses.
  • Transaction History: A list of all transactions, visually differentiated as income (green border) or expense (red border). Each transaction will include a delete option.
  • Add Transaction Form: A form to input new income or expense transactions with descriptions and amounts.
  • Local Storage Persistence: Data will be saved in the browser’s local storage, ensuring data persistence across sessions.
  • Toast Notifications: Using View Toastify to provide user feedback upon adding or deleting transactions.

View Toastify: A Vue.js library used for displaying elegant and non-intrusive notification messages (toasts) to the user.

This project provides a practical context to learn and apply the concepts of Vue.js 3 and the Composition API.

2. Project Setup

To begin, ensure you have Node.js and npm (Node Package Manager) installed on your system. These are essential tools for JavaScript development and managing project dependencies.

Node.js: A JavaScript runtime environment that allows you to run JavaScript code outside of a web browser. It is essential for modern web development tooling.

npm (Node Package Manager): A package manager for JavaScript. It is used to install, manage, and share JavaScript packages and libraries.

2.1 Creating a New Vue.js Project with Vue CLI

We will use the Vue CLI (Command Line Interface) to quickly scaffold a new Vue.js project. If you don’t have Vue CLI installed globally, you can use npx to run it directly without global installation.

Vue CLI (Command Line Interface): A command-line tool for scaffolding and managing Vue.js projects. It simplifies project setup and development workflows.

Open your terminal and navigate to the directory where you want to create your project. Then, run the following command:

npx create-vue@latest .

The . specifies that you want to create the project in the current directory. You will be prompted with a series of questions:

  • Project name: Choose a name for your project (e.g., vue-expense-tracker).
  • Add TypeScript? Select No.
  • Add JSX Support? Select No.
  • Add Vue Router for Single Page Application development? Select No (for this project, a router is not needed).
  • Add Pinia for state management? Select No (for this project, Pinia is not necessary).
  • Add Vitest for Unit testing? Select No.
  • Add Cypress for E2E testing? Select No.
  • Add ESLint for code linting? Select No.

After answering these questions, the Vue CLI will generate the basic project structure. Navigate into your project directory (if you created a subdirectory project) and install the project dependencies using npm:

npm install

To start the development server and see your application in the browser, run:

npm run dev

This will launch a development server, typically accessible at http://localhost:5173 (the port number may vary).

2.2 File Structure Overview

Let’s examine the key files and folders in the generated Vue.js project:

  • package.json: This file contains metadata about your project, including dependencies (libraries your project relies on) and scripts (commands to run development tasks). You’ll see vue listed under dependencies and vite under devDependencies.

    package.json: A JSON file at the root of a Node.js project that describes the project’s dependencies, scripts, and other metadata. It is essential for managing project dependencies and build processes.

  • vite.config.js: Configuration file for Vite, the development server and build tool used by Vue.js. You can customize development server settings here if needed, like changing the port number.

    Vite: A fast build tool and development server for modern web development. It provides features like hot module replacement and optimized builds.

  • index.html: The main HTML file for your Single Page Application (SPA). It includes a <div id="app"> where your Vue.js application will be mounted.

    Single Page Application (SPA): A web application that loads a single HTML page and dynamically updates the content as the user interacts with it, without requiring full page reloads.

  • src/main.js: The entry point for your Vue.js application. It initializes the Vue application, mounts the main component (App.vue), and imports global CSS.

  • src/App.vue: The root component of your application. It serves as the container for all other components and defines the overall application structure.

  • src/assets/: Directory to store assets like CSS files and images.

  • src/components/: Directory to store reusable Vue.js components.

  • public/: Directory for public assets like favicon.ico which are served directly without processing by Vite.

2.3 Cleaning Up and Setting Up Basic Styles

To start with a clean slate, perform the following cleanup:

  1. Delete files in src/assets: Delete base.css, logo.svg, and main.css.

  2. Delete files in src/components: Delete HelloWorld.vue and icons/.

  3. Modify src/App.vue: Remove all existing content within the <template> and <script> tags. Replace the content with:

    <template>
      <div>My App</div>
    </template>
    
    <script setup>
    </script>
    
    <style scoped>
    </style>
  4. Create src/assets/style.css: Create a new file named style.css in the src/assets folder. Copy the CSS styles provided in the transcript (or from the linked GitHub repository) and paste them into this file.

  5. Import style.css in src/main.js: Modify src/main.js to import your newly created CSS file:

    import './assets/style.css'
    
    import { createApp } from 'vue'
    import App from './App.vue'
    
    createApp(App).mount('#app')
  6. Replace public/favicon.ico (Optional): You can replace the default favicon with a custom one as mentioned in the transcript, or keep the default.

After these steps, your basic Vue.js application should be set up with a basic structure and styling applied. Running npm run dev should display “My App” centered on the screen with the styles from style.css.

3. Component Breakdown

To structure the expense tracker application effectively, we will break down the UI into reusable components. This modular approach makes development more organized and maintainable. Based on the application’s design, we will create the following components:

  • Header.vue: Displays the main title of the application (“Expense Tracker”).
  • Balance.vue: Shows the total balance.
  • IncomeExpenses.vue: Displays the income and expense balances side-by-side.
  • TransactionList.vue: Renders the list of transactions, including income and expenses.
  • AddTransaction.vue: Contains the form to add new transactions.

Create these component files within the src/components directory. Each file will have a .vue extension and should use PascalCase naming convention (e.g., Header.vue, Balance.vue).

3.1 Creating Components and Basic Structure

For each component file (Header.vue, Balance.vue, IncomeExpenses.vue, TransactionList.vue, AddTransaction.vue), add the basic Vue.js component structure:

<template>
  <div>
    <!-- Component Content Here -->
  </div>
</template>

<script setup>
  // Component Logic Here (Composition API)
</script>

<style scoped>
  /* Component Specific Styles Here (optional for now) */
</style>

Populating Components with Basic HTML:

Refer to the transcript or the provided GitHub repository to copy the basic HTML structure for each component. Place this HTML within the <template> section of each component file. For example, in Header.vue, it will be a simple <h2> tag. In Balance.vue, it will be an <h4> and an <h1> for the balance amount.

Importing and Registering Components in App.vue:

To use these components in your main application, you need to import and register them in App.vue. Using the <script setup> syntax, component registration is implicit. Simply import the components at the top of App.vue and then use them in the <template> section.

<template>
  <div class="container">
    <Header />
    <Balance />
    <IncomeExpenses />
    <TransactionList />
    <AddTransaction />
  </div>
</template>

<script setup>
import Header from './components/Header.vue';
import Balance from './components/Balance.vue';
import IncomeExpenses from './components/IncomeExpenses.vue';
import TransactionList from './components/TransactionList.vue';
import AddTransaction from './components/AddTransaction.vue';
</script>

Ensure you add a div with the class container in App.vue to match the CSS styling from style.css.

After completing these steps, your application should display the basic layout of the expense tracker, with hardcoded text within each component. The next step is to implement the dynamic functionality and data handling.

4. Implementing Core Functionality

Now we will focus on making the application dynamic and interactive. This involves:

  • Displaying transactions in the TransactionList component.
  • Calculating and displaying the balance in the Balance component.
  • Calculating and displaying income and expenses in the IncomeExpenses component.
  • Implementing the “Add Transaction” form functionality.
  • Adding the “Delete Transaction” functionality.

4.1 Displaying Transactions in TransactionList.vue

We’ll start by displaying a list of transactions in the TransactionList component. For now, we will use hardcoded data in App.vue and pass it down to TransactionList.vue as props.

Props (Properties): Custom attributes you can register on a component. Props allow you to pass data from parent components to child components.

1. Define Transaction Data in App.vue:

In the <script setup> section of App.vue, define a reactive array of transaction objects using ref from Vue.js.

<script setup>
import { ref } from 'vue';
// ... other imports

const transactions = ref([
  { id: 1, text: 'Flower', amount: -20 },
  { id: 2, text: 'Salary', amount: 300 },
  { id: 3, text: 'Book', amount: -10 },
  { id: 4, text: 'Camera', amount: 150 }
]);
</script>

2. Pass Transactions as Props to TransactionList.vue:

In App.vue’s template, bind the transactions ref as a prop to the <TransactionList> component.

<template>
  <div class="container">
    <Header />
    <Balance />
    <IncomeExpenses />
    <TransactionList :transactions="transactions" /> <--- Pass as prop
    <AddTransaction />
  </div>
</template>

3. Define Props in TransactionList.vue:

In TransactionList.vue, use defineProps to declare the transactions prop.

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  transactions: {
    type: Array,
    required: true
  }
});
</script>

4. Render Transactions in TransactionList.vue Template:

Use v-for directive to loop through the transactions prop and render a list item (<li>) for each transaction. Use v-bind:key for efficient list rendering, and v-bind:class to conditionally apply minus or plus class based on the transaction amount.

<template>
  <div>
    <h3>History</h3>
    <ul id="list" class="list">
      <li v-for="transaction in props.transactions" :key="transaction.id" :class="{ minus: transaction.amount < 0, plus: transaction.amount > 0 }">
        {{ transaction.text }} <span>{{ transaction.amount < 0 ? '-' : '+' }}${{ Math.abs(transaction.amount) }}</span><button class="delete-btn">x</button>
      </li>
    </ul>
  </div>
</template>

Now, your application should display the hardcoded transactions in the transaction list. The border color (red or green) and +/- sign will dynamically change based on the amount.

4.2 Calculating and Displaying Balance in Balance.vue

The Balance component needs to calculate the total balance based on the transactions. We’ll use a computed property in App.vue to calculate the total and pass it as a prop to Balance.vue.

Computed Properties: Properties that are derived from existing data. They are cached and only re-evaluated when their dependencies change, making them efficient for complex calculations.

1. Create a Computed Property for Total Balance in App.vue:

Import computed from Vue.js and define a computed property called total that calculates the sum of all transaction amounts.

<script setup>
import { ref, computed } from 'vue'; // Import computed

// ... transactions ref ...

const total = computed(() => {
  return props.transactions.value.reduce((acc, transaction) => acc + transaction.amount, 0);
});
</script>

2. Pass Total as Prop to Balance.vue:

In App.vue’s template, bind the total computed property as a prop to the <Balance> component.

<template>
  <div class="container">
    <Header />
    <Balance :total="total" /> <--- Pass as prop
    <IncomeExpenses />
    <TransactionList :transactions="transactions" />
    <AddTransaction />
  </div>
</template>

3. Define Props in Balance.vue:

In Balance.vue, use defineProps to declare the total prop.

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  total: {
    type: Number,
    required: true
  }
});
</script>

4. Display Total in Balance.vue Template:

In Balance.vue’s template, replace the hardcoded balance with the total prop value.

<template>
  <div>
    <h4>Your Balance</h4>
    <h1>${{ props.total.toFixed(2) }}</h1>
  </div>
</template>

Now, the Balance component should dynamically display the calculated total balance based on the transactions.

4.3 Calculating and Displaying Income and Expenses in IncomeExpenses.vue

Similar to the balance, we will calculate income and expense totals using computed properties in App.vue and pass them as props to IncomeExpenses.vue.

1. Create Computed Properties for Income and Expenses in App.vue:

Define two computed properties, income and expenses, that filter transactions based on whether the amount is positive (income) or negative (expense) and then calculate the sum.

<script setup>
import { ref, computed } from 'vue';
// ... transactions ref ...
// ... total computed property ...

const income = computed(() => {
  return props.transactions.value
    .filter(transaction => transaction.amount > 0)
    .reduce((acc, transaction) => acc + transaction.amount, 0)
    .toFixed(2);
});

const expenses = computed(() => {
  return props.transactions.value
    .filter(transaction => transaction.amount < 0)
    .reduce((acc, transaction) => acc + transaction.amount, 0)
    .toFixed(2);
});
</script>

2. Pass Income and Expenses as Props to IncomeExpenses.vue:

In App.vue’s template, bind the income and expenses computed properties as props to the <IncomeExpenses> component.

<template>
  <div class="container">
    <Header />
    <Balance :total="total" />
    <IncomeExpenses :income="income" :expenses="expenses" /> <--- Pass as props
    <TransactionList :transactions="transactions" />
    <AddTransaction />
  </div>
</template>

3. Define Props in IncomeExpenses.vue:

In IncomeExpenses.vue, use defineProps to declare the income and expenses props.

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  income: {
    type: Number, // Corrected type to Number as we are passing numbers
    required: true
  },
  expenses: {
    type: Number, // Corrected type to Number
    required: true
  }
});
</script>

4. Display Income and Expenses in IncomeExpenses.vue Template:

In IncomeExpenses.vue’s template, replace the hardcoded income and expense values with the income and expenses prop values.

<template>
  <div class="inc-exp-container">
    <div>
      <h4>Income</h4>
      <p id="money-plus" class="money plus">+${{ props.income }}</p>
    </div>
    <div>
      <h4>Expense</h4>
      <p id="money-minus" class="money minus">-${{ props.expenses }}</p>
    </div>
  </div>
</template>

Now, the IncomeExpenses component should dynamically display the calculated income and expense balances.

4.4 Implementing “Add Transaction” Form Functionality in AddTransaction.vue

The AddTransaction component needs to handle user input from the form and emit an event to App.vue when a new transaction is submitted.

1. Set up Form and Input Binding in AddTransaction.vue Template:

Use v-model to bind the input fields (text and amount) to reactive refs in the <script setup> section. Add an @submit.prevent listener to the <form> to handle form submission and prevent page reload.

<template>
  <div>
    <h3>Add new transaction</h3>
    <form id="form" @submit.prevent="onSubmit">
      <div class="form-control">
        <label for="text">Text</label>
        <input type="text" id="text" v-model="text" placeholder="Enter text..." />
      </div>
      <div class="form-control">
        <label for="amount">Amount <br />
          (negative - expense, positive - income)</label>
        <input type="text" id="amount" v-model="amount" placeholder="Enter amount..." />
      </div>
      <button class="btn">Add transaction</button>
    </form>
  </div>
</template>

2. Define Reactive Refs and onSubmit Function in AddTransaction.vue Script:

Import ref and defineEmits from Vue.js. Create reactive refs text and amount to store input values. Define an onSubmit function that handles form submission, performs basic validation, emits a custom event transactionSubmitted with transaction data, and clears the input fields.

<script setup>
import { ref, defineEmits } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast

const text = ref('');
const amount = ref('');
const emit = defineEmits(['transactionSubmitted']); // Define custom event
const toast = useToast(); // Initialize toast

const onSubmit = () => {
  if (!text.value.trim() || !amount.value.trim()) {
    toast.error("Both fields must be filled"); // Use toast for error message
    return;
  }

  const transactionData = {
    text: text.value,
    amount: parseFloat(amount.value) // Parse amount as float
  };

  emit('transactionSubmitted', transactionData); // Emit custom event

  text.value = ''; // Clear input fields
  amount.value = '';
};
</script>

3. Install and Configure View Toastify:

Install vue-toastify using npm:

npm install vue-toastify@next

In src/main.js, import toast and its CSS, and use it in your Vue app.

import './assets/style.css'
import 'vue-toastify/listner.css'; // Corrected import path
import { createApp } from 'vue'
import App from './App.vue'
import { toast } from 'vue-toastify'; // Import toast

const app = createApp(App);
app.use(toast); // Use toast plugin
app.mount('#app');

4. Handle transactionSubmitted Event in App.vue:

In App.vue’s template, listen for the transactionSubmitted custom event on the <AddTransaction> component. Create a handler function handleTransactionSubmitted in App.vue’s script to receive the transaction data, generate a unique ID, add the new transaction to the transactions array, and display a success toast.

<template>
  <div class="container">
    <Header />
    <Balance :total="total" />
    <IncomeExpenses :income="income" :expenses="expenses" />
    <TransactionList :transactions="transactions" />
    <AddTransaction @transactionSubmitted="handleTransactionSubmitted" /> <--- Listen for event
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast
// ... other imports

const transactions = ref([ /* ... initial transactions ... */ ]);
// ... total, income, expenses computed properties ...
const toast = useToast(); // Initialize toast

const handleTransactionSubmitted = (transactionData) => {
  const newTransaction = {
    id: generateUniqueId(), // Function to generate unique ID (implement below)
    ...transactionData // Spread operator to include text and amount
  };
  transactions.value.push(newTransaction);
  toast.success("Transaction added"); // Success toast
};

function generateUniqueId() { // Simple unique ID generator
  return Math.floor(Math.random() * 1000000);
}
</script>

Implement the generateUniqueId function (a simple example is provided in the code above).

Now, you should be able to add new transactions using the form, and the application will reactively update the balance, income/expenses, and transaction list.

4.5 Implementing “Delete Transaction” Functionality in TransactionList.vue and App.vue

To enable transaction deletion, we need to add a delete button to each transaction item in TransactionList.vue and emit an event when it’s clicked. App.vue will handle the event and update the transactions array.

1. Add Delete Button and Click Event in TransactionList.vue Template:

In TransactionList.vue’s template, add a <button class="delete-btn">x</button> within each <li> item. Attach a @click event listener to this button that calls a deleteTransaction function and passes the transaction.id.

<template>
  <div>
    <h3>History</h3>
    <ul id="list" class="list">
      <li v-for="transaction in props.transactions" :key="transaction.id" :class="{ minus: transaction.amount < 0, plus: transaction.amount > 0 }">
        {{ transaction.text }} <span>{{ transaction.amount < 0 ? '-' : '+' }}${{ Math.abs(transaction.amount) }}</span><button class="delete-btn" @click="deleteTransaction(transaction.id)">x</button> <--- Delete button with click event
      </li>
    </ul>
  </div>
</template>

2. Define deleteTransaction Function and Emit transactionDeleted Event in TransactionList.vue Script:

In TransactionList.vue’s <script setup> section, define the deleteTransaction function. This function should use defineEmits to declare a custom event transactionDeleted and emit this event with the transaction ID when the delete button is clicked.

<script setup>
import { defineProps, defineEmits } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast

const props = defineProps({
  transactions: {
    type: Array,
    required: true
  }
});
const emit = defineEmits(['transactionDeleted']); // Define custom event
const toast = useToast(); // Initialize toast

const deleteTransaction = (id) => {
  emit('transactionDeleted', id); // Emit custom event with transaction ID
  toast.success("Transaction deleted"); // Success toast
};
</script>

3. Handle transactionDeleted Event in App.vue:

In App.vue’s template, listen for the transactionDeleted custom event on the <TransactionList> component. Create a handler function handleTransactionDeleted in App.vue’s script to receive the transaction ID and filter out the transaction with that ID from the transactions array.

<template>
  <div class="container">
    <Header />
    <Balance :total="total" />
    <IncomeExpenses :income="income" :expenses="expenses" />
    <TransactionList :transactions="transactions" @transactionDeleted="handleTransactionDeleted" /> <--- Listen for event
    <AddTransaction @transactionSubmitted="handleTransactionSubmitted" />
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast
// ... other imports

const transactions = ref([ /* ... initial transactions ... */ ]);
// ... total, income, expenses computed properties ...
const toast = useToast(); // Initialize toast

const handleTransactionSubmitted = (transactionData) => { /* ... existing function ... */ };
const handleTransactionDeleted = (id) => {
  transactions.value = transactions.value.filter(transaction => transaction.id !== id); // Filter out deleted transaction
  toast.success("Transaction deleted"); // Success toast
};

function generateUniqueId() { /* ... existing function ... */ }
</script>

Now, you should be able to delete transactions by clicking the “x” button next to each transaction item. The application will reactively update all balances and the transaction list.

5. Data Persistence with Local Storage

To make the expense tracker data persistent across browser sessions, we will implement local storage functionality.

Local Storage: A web storage API that allows web applications to store key-value pairs in a web browser with no expiration time. Data stored in local storage persists even after the browser is closed and reopened.

1. Load Transactions from Local Storage on Component Mount in App.vue:

Use the onMounted lifecycle hook in App.vue to check if there are saved transactions in local storage when the component is mounted. If found, parse the JSON string from local storage and set it as the initial value for the transactions ref.

<script setup>
import { ref, computed, onMounted } from 'vue'; // Import onMounted
import { useToast } from 'vue-toastify';
// ... other imports

const transactions = ref([]); // Initialize as empty array

onMounted(() => {
  const savedTransactions = localStorage.getItem('transactions');
  if (savedTransactions) {
    transactions.value = JSON.parse(savedTransactions);
  }
});
// ... other script code ...
</script>

2. Save Transactions to Local Storage Whenever Transactions Change in App.vue:

Create a function saveTransactionsToLocalStorage that stringifies the transactions.value array and saves it to local storage with the key ‘transactions’. Call this function after adding a new transaction in handleTransactionSubmitted and after deleting a transaction in handleTransactionDeleted.

<script setup>
import { ref, computed, onMounted } from 'vue';
import { useToast } from 'vue-toastify';
// ... other imports

const transactions = ref([]);
// ... onMounted hook ...

const handleTransactionSubmitted = (transactionData) => {
  const newTransaction = { /* ... existing code ... */ };
  transactions.value.push(newTransaction);
  saveTransactionsToLocalStorage(); // Save to local storage
  toast.success("Transaction added");
};

const handleTransactionDeleted = (id) => {
  transactions.value = transactions.value.filter(transaction => transaction.id !== id);
  saveTransactionsToLocalStorage(); // Save to local storage
  toast.success("Transaction deleted");
};

function generateUniqueId() { /* ... existing function ... */ }

function saveTransactionsToLocalStorage() {
  localStorage.setItem('transactions', JSON.stringify(transactions.value));
}
</script>

Now, your expense tracker application will persist data in local storage. Transactions will be saved when added or deleted, and they will be loaded from local storage when the application is loaded, even after closing and reopening the browser.

Conclusion

This chapter has provided a comprehensive guide to building an expense tracker application using Vue.js 3 and the Composition API. You have learned how to:

  • Set up a Vue.js 3 project using Vue CLI.
  • Structure your application using reusable components.
  • Utilize the Composition API and <script setup> syntax for component logic.
  • Pass data between components using props.
  • Implement reactivity using ref and computed properties.
  • Handle user input and form submissions.
  • Emit and handle custom events between components.
  • Use View Toastify for user notifications.
  • Persist application data using local storage.

This project serves as a solid foundation for further exploration of Vue.js 3 and front-end development concepts. You can extend this application by adding features like editing transactions, filtering transactions by date, or implementing user authentication.