DEV Community

Nadim Chowdhury
Nadim Chowdhury

Posted on

Building a Full-Stack Personal Finance Tracker with NestJS, Prisma, and Next.js

To develop a Personal Finance Tracker with Next.js, NestJS, PostgreSQL, and Prisma, here’s a breakdown of the features, functionality, and components it could include:

Features and Functionality

1. User Authentication and Profiles

  • Sign-up/Login: Implement user registration and authentication (using JWT or OAuth).
  • Profile Management: Users can update their profiles, including financial goals and settings.
  • Role-based Access: Admin users for managing categories, user data, etc.

2. Transaction Management

  • Add Transactions: Users can manually add their income and expense transactions.
  • Transaction Categorization: Transactions automatically or manually categorized (e.g., groceries, entertainment, bills).
  • Recurring Transactions: Set up recurring transactions like rent or monthly salary.
  • Bulk Import/Export: Ability to import transactions from bank statements (CSV or other formats) and export transaction history for personal records.

3. Expense and Income Tracking

  • Income Sources: Track multiple income streams.
  • Expense Categorization: Track all expenses by category, date, or merchant.
  • Expense Trends: Visual representation of spending over time.
  • Expense Tags: Users can tag expenses (e.g., “vacation,” “gift”) for better organization.

4. Budgeting Tools

  • Create Budgets: Set up monthly/annual budgets by category (e.g., limit $500 for groceries).
  • Track Budget Progress: Users receive real-time feedback on how much budget is left.
  • Budget Alerts: Notifications when spending exceeds a budgeted category or when approaching limits.

5. Financial Reports and Analytics

  • Income/Expense Reports: Generate monthly, quarterly, or yearly reports.
  • Custom Reports: Ability to create custom reports by time range, category, or tags.
  • Spending Insights: Provide insights on spending habits (e.g., largest expenses, unusual spending patterns).
  • Graphs and Charts: Include visual tools (e.g., pie charts, bar graphs) to help users understand their financial status.

6. Goal Setting

  • Create Financial Goals: Users can set financial goals (e.g., save $5000 for a trip).
  • Track Goal Progress: Users can allocate savings or funds towards specific goals.
  • Automated Savings Suggestions: Recommend savings amounts based on income/expenses.

7. Notifications and Alerts

  • Budget Alerts: Notifications for exceeding spending limits or upcoming bills.
  • Goal Progress Alerts: Reminders or notifications when users are on track or falling behind on goals.
  • Recurring Bill Reminders: Alerts for recurring transactions (e.g., rent due).
  • Custom Alerts: Users can set personalized alerts based on financial events or thresholds.

8. Financial Insights Dashboard

  • Overview Page: Summarize key financial data (e.g., net worth, recent transactions, budget status).
  • Top Expense Categories: List of categories where most money is being spent.
  • Income vs. Expenses: Visual comparison of income versus expenses for a given time period.

9. Mobile App Features

  • Real-time Sync: All data synchronized in real-time between web and mobile apps.
  • Quick Transaction Entry: Fast access to log expenses or income from the mobile app.
  • Budget Alerts on Mobile: Push notifications for exceeding budgets or reaching financial goals.
  • Offline Mode: Users can log transactions offline, and they sync when back online.

10. Data Security and Privacy

  • Encrypted Data Storage: All sensitive financial data is encrypted.
  • Two-factor Authentication (2FA): Additional layer of security during login.
  • User Consent for Data: Users can manage their privacy settings and control what data is stored.

Project Architecture

Frontend: Next.js

  • State Management: Use libraries like Redux or Zustand to manage state (e.g., user data, transaction lists).
  • UI Components: Build reusable components for budget tracking, financial reports, transaction tables, etc.
  • Authentication: Integrate JWT or OAuth-based authentication for secure login and access.
  • Responsive Design: Ensure the app works seamlessly on desktop and mobile.
  • API Integration: Next.js will communicate with the NestJS backend through API calls (REST/GraphQL).

Backend: NestJS

  • API Design: Expose REST or GraphQL endpoints for all necessary features (e.g., transaction CRUD, budget tracking, reports).
  • Authentication and Authorization: Implement JWT-based user authentication and role-based access control (RBAC).
  • Background Jobs: Schedule notifications, budget alerts, and periodic reports using NestJS background workers.
  • Data Validation: Use DTOs (Data Transfer Objects) and validation pipelines to ensure proper input data.

Database: PostgreSQL

  • Prisma ORM: Manage the database with Prisma for efficient queries and modeling.
  • Database Schema: Design schemas for users, transactions, budgets, goals, categories, and reports.
  • Database Relations: Handle relationships (e.g., users to transactions, budgets to categories) efficiently using Prisma.
  • Data Aggregation: Use PostgreSQL functions for creating reports and analytics.

Mobile App: React Native (optional extension)

  • Cross-platform Development: The mobile app will reuse the Next.js API endpoints and state management logic.
  • Push Notifications: Implement mobile alerts for budgeting, goal tracking, or expense thresholds.
  • Offline Sync: Add an offline mode that syncs when the user reconnects.

Potential Enhancements

  • Bank Integration: Integrate with APIs like Plaid to automatically pull in bank transactions.
  • Multi-Currency Support: Allow users to track expenses in different currencies.
  • Investment Tracking: Provide tools to track stocks, bonds, or other investments.
  • Tax Estimation: Include tax calculation and estimation tools for better financial planning.

This plan outlines a comprehensive personal finance tracker with robust features across the web and mobile platforms. Let me know if you want to further explore any specific area!

Here’s a suggested folder and file structure for the frontend of your Personal Finance Tracker project using Next.js. This structure is designed to handle all the features and functionality outlined earlier. It follows Next.js conventions while organizing the project for scalability and maintainability.

Project Folder Structure:

personal-finance-tracker-frontend/
├── public/
│   ├── assets/                # Static assets like images, icons, etc.
│   │   └── logo.png
│   └── favicon.ico
├── src/
│   ├── components/            # Reusable UI components
│   │   ├── auth/
│   │   │   ├── LoginForm.tsx
│   │   │   └── RegisterForm.tsx
│   │   ├── dashboard/
│   │   │   ├── BudgetOverview.tsx
│   │   │   ├── ExpenseChart.tsx
│   │   │   ├── IncomeChart.tsx
│   │   │   └── TransactionTable.tsx
│   │   ├── forms/
│   │   │   ├── AddTransactionForm.tsx
│   │   │   └── BudgetForm.tsx
│   │   ├── layouts/
│   │   │   ├── MainLayout.tsx
│   │   │   └── AuthLayout.tsx
│   │   ├── shared/
│   │   │   ├── Button.tsx
│   │   │   ├── Modal.tsx
│   │   │   └── InputField.tsx
│   │   └── notifications/
│   │       └── BudgetAlert.tsx
│   ├── contexts/              # React context for state management
│   │   ├── AuthContext.tsx
│   │   └── BudgetContext.tsx
│   ├── hooks/                 # Custom hooks for reusable logic
│   │   ├── useAuth.tsx
│   │   ├── useTransactions.tsx
│   │   └── useBudgets.tsx
│   ├── pages/                 # Next.js pages and routes
│   │   ├── api/               # Next.js API routes (if any are used)
│   │   ├── auth/              # Authentication-related pages
│   │   │   ├── login.tsx
│   │   │   └── register.tsx
│   │   ├── dashboard/         # Dashboard pages
│   │   │   ├── index.tsx
│   │   │   └── reports.tsx
│   │   ├── goals/             # Financial goals pages
│   │   │   ├── create.tsx
│   │   │   └── index.tsx
│   │   ├── transactions/      # Transactions-related pages
│   │   │   ├── add.tsx
│   │   │   └── index.tsx
│   │   ├── budgets/           # Budgeting pages
│   │   │   ├── create.tsx
│   │   │   └── index.tsx
│   │   ├── index.tsx          # Landing or home page
│   │   └── _app.tsx           # Custom App component (for global settings)
│   ├── services/              # Services for API requests and other logic
│   │   ├── authService.ts     # Auth-related API calls
│   │   ├── transactionService.ts # API calls for transactions
│   │   ├── budgetService.ts   # API calls for budgets
│   │   └── reportService.ts   # API calls for reports
│   ├── store/                 # State management (if using Redux or Zustand)
│   │   ├── index.ts           # Configure store
│   │   ├── authSlice.ts       # Auth state slice
│   │   └── budgetSlice.ts     # Budget state slice
│   ├── styles/                # Global styles (CSS or Tailwind)
│   │   ├── globals.css        # Global styles (or Tailwind imports)
│   │   └── tailwind.config.js # Tailwind configuration (if using)
│   ├── utils/                 # Utility functions/helpers
│   │   ├── formatDate.ts      # Helper to format dates
│   │   ├── validateForm.ts    # Form validation logic
│   │   └── constants.ts       # App-wide constants (e.g., budget categories)
│   └── types/                 # TypeScript types/interfaces
│       ├── transaction.ts     # Types for transactions
│       ├── budget.ts          # Types for budgets
│       └── user.ts            # Types for user/auth
├── .env                       # Environment variables (e.g., API keys)
├── next.config.js             # Next.js configuration
├── tailwind.config.js         # Tailwind CSS configuration (if using)
├── tsconfig.json              # TypeScript configuration
├── package.json               # Dependencies and scripts
└── README.md                  # Project documentation
Enter fullscreen mode Exit fullscreen mode

Breakdown of Key Directories and Files

1. components/

  • This is where all the reusable components live, organized by functionality (e.g., forms, shared, dashboard).
  • Shared Components: Button, Modal, InputField are reusable UI elements.
  • Auth Components: Login and registration forms.
  • Dashboard Components: Contains components to display user-specific financial data (e.g., ExpenseChart, TransactionTable).
  • Notifications: Components like BudgetAlert to notify users when they're close to exceeding their budget.

2. contexts/

  • Handles global state using React Context API for things like user authentication (AuthContext.tsx) and budget management (BudgetContext.tsx).

3. hooks/

  • Custom hooks to abstract and reuse logic across components, such as useTransactions, useAuth, and useBudgets.

4. pages/

  • The pages/ directory is used by Next.js to generate routes. For example:
    • /auth/login maps to login.tsx for the login page.
    • /transactions/add maps to add.tsx for adding a new transaction.
  • _app.tsx is a custom App component where you can wrap global settings like context providers or state management tools (e.g., Redux, Zustand).

5. services/

  • Contains API service modules like authService.ts, transactionService.ts, which handle the actual API calls to your backend (NestJS).
  • This keeps your API logic separate and clean, making it easier to maintain and update.

6. store/

  • If you're using Redux or Zustand for global state management, this folder contains slices of state, such as authSlice.ts for handling user authentication and budgetSlice.ts for budget state.
  • The index.ts file sets up the store and combines reducers.

7. styles/

  • Contains global styles like globals.css. If you're using Tailwind CSS, the tailwind.config.js would go here to customize Tailwind’s default settings.

8. utils/

  • Contains utility functions like formatDate.ts for date formatting and validateForm.ts for form validation logic. constants.ts is where you’d keep shared constants, like categories for transaction types.

9. types/

  • Store your TypeScript interfaces and types here. Examples include transaction.ts, budget.ts, and user.ts for defining the data structures used throughout the app.

Additional Notes:

  • Responsive Design: Ensure all components are responsive to work on different screen sizes, especially if you plan to make this a mobile-first experience.
  • State Management: Choose between Redux, Zustand, or Next.js's built-in useState and useContext depending on the complexity of your app.
  • API Integration: The services layer will call your NestJS backend using Axios or Fetch.
  • Environment Variables: Use .env for storing environment variables like API keys.

This folder structure ensures that the project is well-organized and scalable, making it easier to manage as the application grows.

For the backend of your Personal Finance Tracker project, you will be using NestJS, PostgreSQL, and Prisma. Here's a comprehensive folder and file structure that supports all the features and functionality, ensuring scalability and maintainability.

Backend Folder Structure

personal-finance-tracker-backend/
├── prisma/
│   ├── migrations/                # Prisma migration files
│   ├── schema.prisma              # Prisma schema for database models
│   └── seed.ts                    # Seed data script for initial data
├── src/
│   ├── auth/                      # Authentication module
│   │   ├── auth.controller.ts      # Auth API controllers
│   │   ├── auth.module.ts          # Auth module definition
│   │   ├── auth.service.ts         # Auth service (business logic)
│   │   ├── dto/
│   │   │   └── login.dto.ts        # DTO for login
│   │   └── jwt.strategy.ts         # JWT strategy for authorization
│   ├── users/                     # Users module
│   │   ├── users.controller.ts     # Users API controllers
│   │   ├── users.module.ts         # Users module definition
│   │   ├── users.service.ts        # Users service (business logic)
│   │   └── dto/
│   │       └── create-user.dto.ts  # DTO for creating users
│   ├── transactions/              # Transactions module
│   │   ├── transactions.controller.ts # Transactions API controllers
│   │   ├── transactions.module.ts  # Transactions module definition
│   │   ├── transactions.service.ts # Transactions service (business logic)
│   │   ├── dto/
│   │   │   ├── create-transaction.dto.ts  # DTO for creating a transaction
│   │   │   └── update-transaction.dto.ts  # DTO for updating a transaction
│   │   └── entities/
│   │       └── transaction.entity.ts      # Transaction entity definition
│   ├── budgets/                   # Budgets module
│   │   ├── budgets.controller.ts   # Budgets API controllers
│   │   ├── budgets.module.ts       # Budgets module definition
│   │   ├── budgets.service.ts      # Budgets service (business logic)
│   │   ├── dto/
│   │   │   ├── create-budget.dto.ts       # DTO for creating a budget
│   │   │   └── update-budget.dto.ts       # DTO for updating a budget
│   │   └── entities/
│   │       └── budget.entity.ts           # Budget entity definition
│   ├── reports/                   # Reports module
│   │   ├── reports.controller.ts   # Reports API controllers
│   │   ├── reports.module.ts       # Reports module definition
│   │   ├── reports.service.ts      # Reports service (business logic)
│   ├── goals/                     # Financial goals module
│   │   ├── goals.controller.ts     # Goals API controllers
│   │   ├── goals.module.ts         # Goals module definition
│   │   ├── goals.service.ts        # Goals service (business logic)
│   │   ├── dto/
│   │   │   ├── create-goal.dto.ts         # DTO for creating a financial goal
│   │   │   └── update-goal.dto.ts         # DTO for updating a goal
│   │   └── entities/
│   │       └── goal.entity.ts             # Goal entity definition
│   ├── notifications/             # Notifications module
│   │   ├── notifications.controller.ts    # Notifications API controllers
│   │   ├── notifications.module.ts        # Notifications module definition
│   │   ├── notifications.service.ts       # Notification services (business logic)
│   ├── config/                    # App configuration files
│   │   ├── config.module.ts        # Configuration module
│   │   └── config.service.ts       # Service to access environment variables
│   ├── common/                    # Common helpers, decorators, interceptors
│   │   ├── decorators/
│   │   │   └── roles.decorator.ts  # Role-based access control decorators
│   │   └── filters/
│   │       └── all-exception.filter.ts    # Global exception handling filter
│   ├── app.module.ts              # Root application module
│   ├── main.ts                    # Entry point of the application
├── test/                          # Unit and E2E tests
│   ├── auth/
│   │   └── auth.controller.spec.ts # Auth controller test cases
│   ├── transactions/
│   │   └── transactions.controller.spec.ts # Transactions test cases
│   └── app.e2e-spec.ts            # End-to-end testing
├── .env                           # Environment variables (API keys, DB config)
├── nest-cli.json                  # Nest CLI configuration
├── tsconfig.json                  # TypeScript configuration
├── package.json                   # Dependencies and scripts
└── README.md                      # Project documentation
Enter fullscreen mode Exit fullscreen mode

Breakdown of Key Directories and Files

1. prisma/

  • schema.prisma: Defines the data models for your project (e.g., users, transactions, budgets, etc.). It is your primary interface for managing database schema.
  • migrations/: Prisma will automatically create migration files in this folder when running prisma migrate.
  • seed.ts: A script to seed initial data into your database (e.g., default categories, sample users).

2. src/

  • The main source directory where all the business logic, modules, services, controllers, and entities are defined.

3. Modules

a. Authentication (auth/)
  • auth.controller.ts: Defines the routes for login, registration, and token refresh.
  • auth.service.ts: Contains business logic for handling user authentication (JWT generation, validation).
  • jwt.strategy.ts: JWT strategy for securing routes and managing authorization.
b. Users (users/)
  • users.controller.ts: Handles routes related to user management (e.g., fetching user profiles).
  • users.service.ts: Contains business logic for managing users (CRUD operations).
  • create-user.dto.ts: DTO (Data Transfer Object) for user registration.
c. Transactions (transactions/)
  • transactions.controller.ts: Defines routes for managing income and expense transactions (CRUD operations).
  • transactions.service.ts: Business logic for handling transactions, including categorization and totals.
  • transaction.entity.ts: Prisma model for transactions.
d. Budgets (budgets/)
  • budgets.controller.ts: Defines routes for budget management (creating and updating budgets).
  • budgets.service.ts: Business logic for tracking budget progress and spending limits.
  • budget.entity.ts: Prisma model for budget data.
e. Reports (reports/)
  • reports.controller.ts: Defines routes for generating financial reports.
  • reports.service.ts: Business logic for income and expense analytics, spending patterns, and generating reports.
f. Financial Goals (goals/)
  • goals.controller.ts: Handles routes for managing financial goals.
  • goals.service.ts: Business logic for goal tracking and progress updates.
  • goal.entity.ts: Prisma model for financial goals.
g. Notifications (notifications/)
  • notifications.controller.ts: Defines routes for sending alerts (e.g., budget threshold alerts, goal progress notifications).
  • notifications.service.ts: Manages sending notifications via email or push notifications.

4. config/

  • config.module.ts: Centralizes environment configuration, including database connection, JWT secret, and other environment variables.
  • config.service.ts: Provides access to environment variables and configuration throughout the app.

5. common/

  • This directory contains reusable decorators, interceptors, and filters.
  • Decorators: Example is the roles.decorator.ts for applying role-based access control.
  • Filters: Example is the all-exception.filter.ts for handling global exceptions and errors.

6. Entry Files

  • app.module.ts: The root module where all other modules (Auth, Users, Transactions, etc.) are imported.
  • main.ts: The entry point of the application, which starts the NestJS server and applies middleware.

7. test/

  • Contains unit tests and end-to-end (E2E) tests for the controllers and services.
  • Each module (auth, transactions, etc.) will have its own set of test files.

8. Environment Variables (.env)

  • The .env file will hold sensitive configurations like your database URL, JWT secret, and other environment-specific settings.
   DATABASE_URL=postgres://user:password@localhost:5432/personal-finance-tracker
   JWT_SECRET=your_jwt_secret
Enter fullscreen mode Exit fullscreen mode

Key Features for Each Module:

  • Authentication: JWT-based authentication with secure login, registration, and role management.
  • Transaction Management: Full CRUD for income and expense transactions with category management.
  • Budget Tracking: Users can create, update, and track budgets in different categories. Real-time budget alerts and notifications.
  • Financial Goals: Users can set financial goals, track progress, and receive updates or suggestions on how to meet those goals.
  • Reports: Generate custom reports with graphs and charts to help users analyze spending patterns.
  • Notifications: Email or push notifications for budget alerts, upcoming bills, or goal progress.

This backend structure is well-suited for a Personal Finance Tracker, covering all major aspects like transactions, budgeting, goals, and notifications, while maintaining a clear separation of concerns.

Here’s a full implementation for the components you mentioned, styled with Tailwind CSS.

1. LoginForm.tsx

import { useState } from "react";

const LoginForm = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // handle login logic
  };

  return (
    <div className="flex items-center justify-center h-screen bg-gray-100">
      <form
        onSubmit={handleSubmit}
        className="bg-white p-6 rounded-lg shadow-lg w-full max-w-sm"
      >
        <h2 className="text-2xl font-semibold text-center mb-6">Login</h2>
        <div className="mb-4">
          <label htmlFor="email" className="block text-gray-700">Email</label>
          <input
            id="email"
            type="email"
            className="w-full mt-2 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="mb-4">
          <label htmlFor="password" className="block text-gray-700">Password</label>
          <input
            id="password"
            type="password"
            className="w-full mt-2 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button
          type="submit"
          className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
        >
          Login
        </button>
      </form>
    </div>
  );
};

export default LoginForm;
Enter fullscreen mode Exit fullscreen mode

2. RegisterForm.tsx

import { useState } from "react";

const RegisterForm = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // handle registration logic
  };

  return (
    <div className="flex items-center justify-center h-screen bg-gray-100">
      <form
        onSubmit={handleSubmit}
        className="bg-white p-6 rounded-lg shadow-lg w-full max-w-sm"
      >
        <h2 className="text-2xl font-semibold text-center mb-6">Register</h2>
        <div className="mb-4">
          <label htmlFor="email" className="block text-gray-700">Email</label>
          <input
            id="email"
            type="email"
            className="w-full mt-2 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="mb-4">
          <label htmlFor="password" className="block text-gray-700">Password</label>
          <input
            id="password"
            type="password"
            className="w-full mt-2 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <div className="mb-4">
          <label htmlFor="confirmPassword" className="block text-gray-700">Confirm Password</label>
          <input
            id="confirmPassword"
            type="password"
            className="w-full mt-2 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
          />
        </div>
        <button
          type="submit"
          className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
        >
          Register
        </button>
      </form>
    </div>
  );
};

export default RegisterForm;
Enter fullscreen mode Exit fullscreen mode

3. BudgetOverview.tsx

const BudgetOverview = () => {
  return (
    <div className="bg-white p-6 rounded-lg shadow-lg">
      <h2 className="text-xl font-semibold mb-4">Budget Overview</h2>
      <p>Total Budget: $5,000</p>
      <p>Total Spent: $2,500</p>
      <p>Remaining: $2,500</p>
    </div>
  );
};

export default BudgetOverview;
Enter fullscreen mode Exit fullscreen mode

4. ExpenseChart.tsx

For charts, I suggest using a library like Chart.js. Here’s how you could implement an expense chart using react-chartjs-2:

First, install the dependencies:

npm install react-chartjs-2 chart.js
Enter fullscreen mode Exit fullscreen mode

Then create the component:

import { Pie } from "react-chartjs-2";
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';

ChartJS.register(ArcElement, Tooltip, Legend);

const ExpenseChart = () => {
  const data = {
    labels: ["Rent", "Groceries", "Entertainment", "Transport", "Miscellaneous"],
    datasets: [
      {
        label: "Expenses",
        data: [1200, 300, 200, 150, 50],
        backgroundColor: [
          "#f87171",
          "#60a5fa",
          "#fbbf24",
          "#34d399",
          "#f472b6",
        ],
      },
    ],
  };

  return (
    <div className="bg-white p-6 rounded-lg shadow-lg">
      <h2 className="text-xl font-semibold mb-4">Expense Breakdown</h2>
      <Pie data={data} />
    </div>
  );
};

export default ExpenseChart;
Enter fullscreen mode Exit fullscreen mode

5. IncomeChart.tsx

Similarly, you can create the IncomeChart component using Chart.js for an income bar chart:

import { Bar } from "react-chartjs-2";
import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';

ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);

const IncomeChart = () => {
  const data = {
    labels: ["Salary", "Freelance", "Investments"],
    datasets: [
      {
        label: "Income",
        data: [4000, 1000, 500],
        backgroundColor: "#4ade80",
      },
    ],
  };

  return (
    <div className="bg-white p-6 rounded-lg shadow-lg">
      <h2 className="text-xl font-semibold mb-4">Income Overview</h2>
      <Bar data={data} />
    </div>
  );
};

export default IncomeChart;
Enter fullscreen mode Exit fullscreen mode

6. TransactionTable.tsx

const TransactionTable = () => {
  const transactions = [
    { id: 1, description: "Groceries", amount: -50, date: "2024-10-01" },
    { id: 2, description: "Salary", amount: 500, date: "2024-10-02" },
    { id: 3, description: "Netflix Subscription", amount: -12, date: "2024-10-03" },
  ];

  return (
    <div className="bg-white p-6 rounded-lg shadow-lg">
      <h2 className="text-xl font-semibold mb-4">Recent Transactions</h2>
      <table className="min-w-full bg-white">
        <thead>
          <tr>
            <th className="py-2 px-4 border-b">Description</th>
            <th className="py-2 px-4 border-b">Amount</th>
            <th className="py-2 px-4 border-b">Date</th>
          </tr>
        </thead>
        <tbody>
          {transactions.map((transaction) => (
            <tr key={transaction.id}>
              <td className="py-2 px-4 border-b">{transaction.description}</td>
              <td className={`py-2 px-4 border-b ${transaction.amount < 0 ? "text-red-500" : "text-green-500"}`}>
                {transaction.amount < 0 ? "-" : "+"}${Math.abs(transaction.amount)}
              </td>
              <td className="py-2 px-4 border-b">{transaction.date}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default TransactionTable;
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS Configuration

Ensure you have Tailwind CSS installed and configured. Here’s how to configure Tailwind in your project:

  1. Install Tailwind:
   npm install -D tailwindcss postcss autoprefixer
   npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode
  1. Configure tailwind.config.js:
   module.exports = {
     content: [
       "./src/**/*.{js,jsx,ts

,tsx}",
     ],
     theme: {
       extend: {},
     },
     plugins: [],
   }
Enter fullscreen mode Exit fullscreen mode
  1. Add Tailwind to your globals.css file:
   @tailwind base;
   @tailwind components;
   @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

With this setup, you now have the LoginForm, RegisterForm, BudgetOverview, ExpenseChart, IncomeChart, and TransactionTable components, styled using Tailwind CSS. You can expand and further customize the components as needed for your personal finance tracker project.

Here’s the full code implementation for the components you requested:

1. AddTransactionForm.tsx

import { useState } from "react";

const AddTransactionForm = () => {
  const [description, setDescription] = useState("");
  const [amount, setAmount] = useState("");
  const [date, setDate] = useState("");
  const [type, setType] = useState("expense"); // "expense" or "income"

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // handle transaction submission logic
  };

  return (
    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg">
      <h2 className="text-2xl font-semibold mb-6">Add Transaction</h2>
      <div className="mb-4">
        <label className="block text-gray-700">Description</label>
        <input
          type="text"
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
        />
      </div>
      <div className="mb-4">
        <label className="block text-gray-700">Amount</label>
        <input
          type="number"
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
      </div>
      <div className="mb-4">
        <label className="block text-gray-700">Date</label>
        <input
          type="date"
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={date}
          onChange={(e) => setDate(e.target.value)}
        />
      </div>
      <div className="mb-4">
        <label className="block text-gray-700">Type</label>
        <select
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={type}
          onChange={(e) => setType(e.target.value)}
        >
          <option value="expense">Expense</option>
          <option value="income">Income</option>
        </select>
      </div>
      <button type="submit" className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition">
        Add Transaction
      </button>
    </form>
  );
};

export default AddTransactionForm;
Enter fullscreen mode Exit fullscreen mode

2. BudgetForm.tsx

import { useState } from "react";

const BudgetForm = () => {
  const [category, setCategory] = useState("");
  const [amount, setAmount] = useState("");
  const [startDate, setStartDate] = useState("");
  const [endDate, setEndDate] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // handle budget creation logic
  };

  return (
    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg">
      <h2 className="text-2xl font-semibold mb-6">Create Budget</h2>
      <div className="mb-4">
        <label className="block text-gray-700">Category</label>
        <input
          type="text"
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={category}
          onChange={(e) => setCategory(e.target.value)}
        />
      </div>
      <div className="mb-4">
        <label className="block text-gray-700">Amount</label>
        <input
          type="number"
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
      </div>
      <div className="mb-4">
        <label className="block text-gray-700">Start Date</label>
        <input
          type="date"
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={startDate}
          onChange={(e) => setStartDate(e.target.value)}
        />
      </div>
      <div className="mb-4">
        <label className="block text-gray-700">End Date</label>
        <input
          type="date"
          className="w-full mt-2 p-2 border border-gray-300 rounded"
          value={endDate}
          onChange={(e) => setEndDate(e.target.value)}
        />
      </div>
      <button type="submit" className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition">
        Create Budget
      </button>
    </form>
  );
};

export default BudgetForm;
Enter fullscreen mode Exit fullscreen mode

3. MainLayout.tsx

import { ReactNode } from "react";

interface MainLayoutProps {
  children: ReactNode;
}

const MainLayout = ({ children }: MainLayoutProps) => {
  return (
    <div className="min-h-screen flex flex-col">
      <header className="bg-blue-600 text-white p-4">
        <h1 className="text-2xl">Personal Finance Tracker</h1>
      </header>
      <main className="flex-grow bg-gray-100 p-6">{children}</main>
      <footer className="bg-blue-600 text-white p-4 text-center">
        <p>&copy; 2024 Personal Finance Tracker</p>
      </footer>
    </div>
  );
};

export default MainLayout;
Enter fullscreen mode Exit fullscreen mode

4. AuthLayout.tsx

import { ReactNode } from "react";

interface AuthLayoutProps {
  children: ReactNode;
}

const AuthLayout = ({ children }: AuthLayoutProps) => {
  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-100">
      <div className="w-full max-w-md bg-white p-6 rounded-lg shadow-lg">{children}</div>
    </div>
  );
};

export default AuthLayout;
Enter fullscreen mode Exit fullscreen mode

5. Button.tsx

interface ButtonProps {
  label: string;
  onClick: () => void;
  type?: "button" | "submit";
}

const Button = ({ label, onClick, type = "button" }: ButtonProps) => {
  return (
    <button
      type={type}
      onClick={onClick}
      className="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition"
    >
      {label}
    </button>
  );
};

export default Button;
Enter fullscreen mode Exit fullscreen mode

6. Modal.tsx

import { ReactNode } from "react";

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  children: ReactNode;
}

const Modal = ({ isOpen, onClose, children }: ModalProps) => {
  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
      <div className="bg-white p-6 rounded-lg shadow-lg">
        <button
          className="absolute top-2 right-2 text-gray-600 hover:text-gray-800"
          onClick={onClose}
        >
          &times;
        </button>
        {children}
      </div>
    </div>
  );
};

export default Modal;
Enter fullscreen mode Exit fullscreen mode

7. InputField.tsx

interface InputFieldProps {
  label: string;
  type: string;
  value: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const InputField = ({ label, type, value, onChange }: InputFieldProps) => {
  return (
    <div className="mb-4">
      <label className="block text-gray-700">{label}</label>
      <input
        type={type}
        className="w-full mt-2 p-2 border border-gray-300 rounded"
        value={value}
        onChange={onChange}
      />
    </div>
  );
};

export default InputField;
Enter fullscreen mode Exit fullscreen mode

8. BudgetAlert.tsx

interface BudgetAlertProps {
  message: string;
  onClose: () => void;
}

const BudgetAlert = ({ message, onClose }: BudgetAlertProps) => {
  return (
    <div className="fixed top-4 right-4 bg-red-500 text-white p-4 rounded-lg shadow-lg">
      <p>{message}</p>
      <button
        className="text-white ml-4"
        onClick={onClose}
      >
        &times;
      </button>
    </div>
  );
};

export default BudgetAlert;
Enter fullscreen mode Exit fullscreen mode

Summary

  • AddTransactionForm.tsx and BudgetForm.tsx provide forms for adding transactions and creating budgets.
  • MainLayout.tsx and AuthLayout.tsx serve as layouts for authenticated and unauthenticated pages.
  • Button.tsx, Modal.tsx, and InputField.tsx are reusable shared components.
  • **

BudgetAlert.tsx** is a notification component to alert users about budget-related events.

Each component is styled with Tailwind CSS and is modular, allowing you to easily reuse and expand functionality across your application. Let me know if you need more!

Here’s the full code implementation for the React Context and custom hooks you requested.


1. AuthContext.tsx

This file provides authentication context to manage the user login state and authentication logic.

import { createContext, useContext, useState, ReactNode, useEffect } from "react";
import { useRouter } from "next/router";

// Define the shape of the context
interface AuthContextType {
  user: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// Create the AuthContext
const AuthContext = createContext<AuthContextType | undefined>(undefined);

// AuthProvider component
export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [user, setUser] = useState<string | null>(null);
  const router = useRouter();

  // Simulate login function
  const login = async (email: string, password: string) => {
    // Mock login logic (replace with API call)
    if (email === "user@example.com" && password === "password") {
      setUser(email);
      router.push("/dashboard");
    } else {
      throw new Error("Invalid credentials");
    }
  };

  // Logout function
  const logout = () => {
    setUser(null);
    router.push("/auth/login");
  };

  useEffect(() => {
    // Simulate retrieving logged in user (e.g., from local storage)
    const storedUser = localStorage.getItem("user");
    if (storedUser) {
      setUser(storedUser);
    }
  }, []);

  useEffect(() => {
    // Simulate persisting user login state
    if (user) {
      localStorage.setItem("user", user);
    } else {
      localStorage.removeItem("user");
    }
  }, [user]);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

// Custom hook to use the AuthContext
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
};
Enter fullscreen mode Exit fullscreen mode

2. BudgetContext.tsx

This file provides context for managing user budgets across the application.

import { createContext, useContext, useState, ReactNode } from "react";

// Define the shape of the context
interface Budget {
  id: string;
  category: string;
  amount: number;
  startDate: string;
  endDate: string;
}

interface BudgetContextType {
  budgets: Budget[];
  addBudget: (budget: Budget) => void;
  removeBudget: (id: string) => void;
}

// Create the BudgetContext
const BudgetContext = createContext<BudgetContextType | undefined>(undefined);

// BudgetProvider component
export const BudgetProvider = ({ children }: { children: ReactNode }) => {
  const [budgets, setBudgets] = useState<Budget[]>([]);

  // Function to add a new budget
  const addBudget = (budget: Budget) => {
    setBudgets((prevBudgets) => [...prevBudgets, budget]);
  };

  // Function to remove a budget
  const removeBudget = (id: string) => {
    setBudgets((prevBudgets) => prevBudgets.filter((budget) => budget.id !== id));
  };

  return (
    <BudgetContext.Provider value={{ budgets, addBudget, removeBudget }}>
      {children}
    </BudgetContext.Provider>
  );
};

// Custom hook to use the BudgetContext
export const useBudgets = () => {
  const context = useContext(BudgetContext);
  if (context === undefined) {
    throw new Error("useBudgets must be used within a BudgetProvider");
  }
  return context;
};
Enter fullscreen mode Exit fullscreen mode

3. useAuth.tsx

This hook is a convenience wrapper around the AuthContext for handling authentication logic in components.

import { useAuth } from "../contexts/AuthContext";

export const useAuth = () => {
  const { user, login, logout } = useAuth();

  const handleLogin = async (email: string, password: string) => {
    try {
      await login(email, password);
    } catch (error) {
      console.error("Login failed", error);
    }
  };

  const handleLogout = () => {
    logout();
  };

  return {
    user,
    handleLogin,
    handleLogout,
  };
};
Enter fullscreen mode Exit fullscreen mode

4. useTransactions.tsx

This hook provides logic for managing transactions (e.g., adding, removing, fetching).

import { useState } from "react";

// Define transaction type
interface Transaction {
  id: string;
  description: string;
  amount: number;
  date: string;
  type: "income" | "expense";
}

export const useTransactions = () => {
  const [transactions, setTransactions] = useState<Transaction[]>([]);

  // Function to add a new transaction
  const addTransaction = (transaction: Transaction) => {
    setTransactions((prevTransactions) => [...prevTransactions, transaction]);
  };

  // Function to remove a transaction by ID
  const removeTransaction = (id: string) => {
    setTransactions((prevTransactions) =>
      prevTransactions.filter((transaction) => transaction.id !== id)
    );
  };

  // Simulated fetchTransactions function (replace with API call)
  const fetchTransactions = () => {
    // Mock transactions data
    const mockTransactions: Transaction[] = [
      { id: "1", description: "Groceries", amount: -50, date: "2024-10-01", type: "expense" },
      { id: "2", description: "Salary", amount: 500, date: "2024-10-02", type: "income" },
      { id: "3", description: "Netflix", amount: -12, date: "2024-10-03", type: "expense" },
    ];
    setTransactions(mockTransactions);
  };

  return {
    transactions,
    addTransaction,
    removeTransaction,
    fetchTransactions,
  };
};
Enter fullscreen mode Exit fullscreen mode

5. useBudgets.tsx

This hook interacts with the BudgetContext for managing budgets in your components.

import { useBudgets } from "../contexts/BudgetContext";

export const useBudgets = () => {
  const { budgets, addBudget, removeBudget } = useBudgets();

  const handleAddBudget = (category: string, amount: number, startDate: string, endDate: string) => {
    const newBudget = {
      id: Date.now().toString(),
      category,
      amount,
      startDate,
      endDate,
    };
    addBudget(newBudget);
  };

  const handleRemoveBudget = (id: string) => {
    removeBudget(id);
  };

  return {
    budgets,
    handleAddBudget,
    handleRemoveBudget,
  };
};
Enter fullscreen mode Exit fullscreen mode

Explanation of Code

  • AuthContext.tsx: Manages user authentication state (login/logout) and provides a global authentication context for the app.
  • BudgetContext.tsx: Manages budget state (add, remove) and provides a global budget context for the app.
  • useAuth.tsx: A custom hook that abstracts authentication logic for easier use in components.
  • useTransactions.tsx: A custom hook for managing transactions in your app, including adding/removing transactions and fetching mock data.
  • useBudgets.tsx: A custom hook for managing budgets, allowing components to easily add and remove budgets through the BudgetContext.

This structure makes it easy to maintain state across your application while keeping logic encapsulated and reusable. Let me know if you need further adjustments or explanations!

Here’s a full implementation for the Next.js pages and routes you requested, covering authentication, the dashboard, goals, transactions, budgets, and the home page.

1. auth/login.tsx

import { useState } from "react";
import { useAuth } from "../../contexts/AuthContext";

const LoginPage = () => {
  const { handleLogin } = useAuth();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await handleLogin(email, password);
    } catch (err) {
      setError("Login failed. Please check your credentials.");
    }
  };

  return (
    <div className="flex items-center justify-center h-screen bg-gray-100">
      <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
        <h2 className="text-2xl font-semibold text-center mb-6">Login</h2>
        {error && <p className="text-red-500 text-center mb-4">{error}</p>}
        <div className="mb-4">
          <label className="block text-gray-700">Email</label>
          <input
            type="email"
            className="w-full mt-2 p-2 border border-gray-300 rounded"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="mb-4">
          <label className="block text-gray-700">Password</label>
          <input
            type="password"
            className="w-full mt-2 p-2 border border-gray-300 rounded"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit" className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition">
          Login
        </button>
      </form>
    </div>
  );
};

export default LoginPage;
Enter fullscreen mode Exit fullscreen mode

2. auth/register.tsx

import { useState } from "react";
import { useAuth } from "../../contexts/AuthContext";

const RegisterPage = () => {
  const { handleLogin } = useAuth();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (password !== confirmPassword) {
      setError("Passwords do not match.");
      return;
    }
    try {
      await handleLogin(email, password);
    } catch (err) {
      setError("Registration failed. Please try again.");
    }
  };

  return (
    <div className="flex items-center justify-center h-screen bg-gray-100">
      <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
        <h2 className="text-2xl font-semibold text-center mb-6">Register</h2>
        {error && <p className="text-red-500 text-center mb-4">{error}</p>}
        <div className="mb-4">
          <label className="block text-gray-700">Email</label>
          <input
            type="email"
            className="w-full mt-2 p-2 border border-gray-300 rounded"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="mb-4">
          <label className="block text-gray-700">Password</label>
          <input
            type="password"
            className="w-full mt-2 p-2 border border-gray-300 rounded"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <div className="mb-4">
          <label className="block text-gray-700">Confirm Password</label>
          <input
            type="password"
            className="w-full mt-2 p-2 border border-gray-300 rounded"
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
          />
        </div>
        <button type="submit" className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition">
          Register
        </button>
      </form>
    </div>
  );
};

export default RegisterPage;
Enter fullscreen mode Exit fullscreen mode

3. dashboard/index.tsx

import BudgetOverview from "../../components/dashboard/BudgetOverview";
import TransactionTable from "../../components/dashboard/TransactionTable";
import ExpenseChart from "../../components/dashboard/ExpenseChart";
import IncomeChart from "../../components/dashboard/IncomeChart";

const DashboardPage = () => {
  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Dashboard</h1>
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
        <BudgetOverview />
        <ExpenseChart />
        <IncomeChart />
      </div>
      <TransactionTable />
    </div>
  );
};

export default DashboardPage;
Enter fullscreen mode Exit fullscreen mode

4. dashboard/reports.tsx

const ReportsPage = () => {
  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Reports</h1>
      <p>Here you can view detailed financial reports and analysis.</p>
    </div>
  );
};

export default ReportsPage;
Enter fullscreen mode Exit fullscreen mode

5. goals/create.tsx

import { useState } from "react";

const CreateGoalPage = () => {
  const [goal, setGoal] = useState("");
  const [amount, setAmount] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // handle goal creation logic
  };

  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Create Financial Goal</h1>
      <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
        <div className="mb-4">
          <label className="block text-gray-700">Goal</label>
          <input
            type="text"
            className="w-full mt-2 p-2 border border-gray-300 rounded"
            value={goal}
            onChange={(e) => setGoal(e.target.value)}
          />
        </div>
        <div className="mb-4">
          <label className="block text-gray-700">Amount</label>
          <input
            type="number"
            className="w-full mt-2 p-2 border border-gray-300 rounded"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
          />
        </div>
        <button type="submit" className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition">
          Create Goal
        </button>
      </form>
    </div>
  );
};

export default CreateGoalPage;
Enter fullscreen mode Exit fullscreen mode

6. goals/index.tsx

const GoalsPage = () => {
  const goals = [
    { id: 1, name: "Save for a new car", amount: 5000 },
    { id: 2, name: "Emergency Fund", amount: 1000 },
  ];

  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Financial Goals</h1>
      <ul>
        {goals.map((goal) => (
          <li key={goal.id} className="mb-4 p-4 bg-white rounded-lg shadow-lg">
            <p>{goal.name}</p>
            <p>Target Amount: ${goal.amount}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default GoalsPage;
Enter fullscreen mode Exit fullscreen mode

7. transactions/add.tsx

import AddTransactionForm from "../../components/forms/AddTransactionForm";

const AddTransactionPage = () => {
  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Add Transaction</h1>
      <AddTransactionForm />
    </div>
  );
};

export default AddTransactionPage;
Enter fullscreen mode Exit fullscreen mode

8. transactions/index.tsx

const TransactionsPage = () => {
  const transactions = [
    { id: 1, description: "Groceries", amount: -50, date: "2024-10-01" },
    { id: 2, description: "Salary", amount: 500, date

: "2024-10-02" },
  ];

  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Transactions</h1>
      <ul>
        {transactions.map((transaction) => (
          <li key={transaction.id} className="mb-4 p-4 bg-white rounded-lg shadow-lg">
            <p>{transaction.description}</p>
            <p>Amount: ${transaction.amount}</p>
            <p>Date: {transaction.date}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TransactionsPage;
Enter fullscreen mode Exit fullscreen mode

9. budgets/create.tsx

import BudgetForm from "../../components/forms/BudgetForm";

const CreateBudgetPage = () => {
  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Create Budget</h1>
      <BudgetForm />
    </div>
  );
};

export default CreateBudgetPage;
Enter fullscreen mode Exit fullscreen mode

10. budgets/index.tsx

const BudgetsPage = () => {
  const budgets = [
    { id: 1, category: "Groceries", amount: 300 },
    { id: 2, category: "Entertainment", amount: 100 },
  ];

  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-3xl font-semibold mb-6">Budgets</h1>
      <ul>
        {budgets.map((budget) => (
          <li key={budget.id} className="mb-4 p-4 bg-white rounded-lg shadow-lg">
            <p>Category: {budget.category}</p>
            <p>Amount: ${budget.amount}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default BudgetsPage;
Enter fullscreen mode Exit fullscreen mode

11. index.tsx (Landing Page)

import Link from "next/link";

const HomePage = () => {
  return (
    <div className="h-screen flex flex-col justify-center items-center bg-gray-100">
      <h1 className="text-4xl font-bold mb-6">Welcome to Personal Finance Tracker</h1>
      <div className="space-x-4">
        <Link href="/auth/login">
          <a className="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition">
            Login
          </a>
        </Link>
        <Link href="/auth/register">
          <a className="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600 transition">
            Register
          </a>
        </Link>
      </div>
    </div>
  );
};

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

Summary

  • Authentication pages (login.tsx, register.tsx) handle user login and registration.
  • Dashboard (dashboard/index.tsx) includes components like budget overview, charts, and a transaction table.
  • Goals pages allow users to create and view financial goals.
  • Transaction and Budget pages handle adding and viewing transactions and budgets.
  • Landing page offers links to log in or register.

All pages are styled with Tailwind CSS and are ready for Next.js. Let me know if you need further customizations or explanations!

Here’s the full code implementation for the services, Redux slices, styles, utility functions, and types you requested.


1. Services

authService.ts

This service handles authentication-related API calls.

import axios from "axios";

const API_URL = "/api/auth";

export const login = async (email: string, password: string) => {
  const response = await axios.post(`${API_URL}/login`, { email, password });
  return response.data;
};

export const register = async (email: string, password: string) => {
  const response = await axios.post(`${API_URL}/register`, { email, password });
  return response.data;
};

export const logout = async () => {
  // Handle token removal logic
};
Enter fullscreen mode Exit fullscreen mode

transactionService.ts

This service handles API calls related to transactions.

import axios from "axios";

const API_URL = "/api/transactions";

export const getTransactions = async () => {
  const response = await axios.get(API_URL);
  return response.data;
};

export const addTransaction = async (transaction: any) => {
  const response = await axios.post(API_URL, transaction);
  return response.data;
};

export const deleteTransaction = async (id: string) => {
  const response = await axios.delete(`${API_URL}/${id}`);
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

budgetService.ts

This service handles API calls related to budgets.

import axios from "axios";

const API_URL = "/api/budgets";

export const getBudgets = async () => {
  const response = await axios.get(API_URL);
  return response.data;
};

export const createBudget = async (budget: any) => {
  const response = await axios.post(API_URL, budget);
  return response.data;
};

export const deleteBudget = async (id: string) => {
  const response = await axios.delete(`${API_URL}/${id}`);
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

reportService.ts

This service handles API calls related to generating reports.

import axios from "axios";

const API_URL = "/api/reports";

export const getReports = async () => {
  const response = await axios.get(API_URL);
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

2. Redux State Management

For state management, I'll use Redux Toolkit for this example.

index.ts

This file configures the Redux store.

import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./authSlice";
import budgetReducer from "./budgetSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    budgets: budgetReducer,
  },
});

export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Enter fullscreen mode Exit fullscreen mode

authSlice.ts

This file manages authentication state.

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { login, register } from "../services/authService";

interface AuthState {
  user: string | null;
  loading: boolean;
  error: string | null;
}

const initialState: AuthState = {
  user: null,
  loading: false,
  error: null,
};

export const loginUser = createAsyncThunk(
  "auth/login",
  async ({ email, password }: { email: string; password: string }, thunkAPI) => {
    try {
      const response = await login(email, password);
      return response;
    } catch (error) {
      return thunkAPI.rejectWithValue("Invalid credentials");
    }
  }
);

export const registerUser = createAsyncThunk(
  "auth/register",
  async ({ email, password }: { email: string; password: string }, thunkAPI) => {
    try {
      const response = await register(email, password);
      return response;
    } catch (error) {
      return thunkAPI.rejectWithValue("Registration failed");
    }
  }
);

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    logout: (state) => {
      state.user = null;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.user = action.payload;
        state.loading = false;
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      });
  },
});

export const { logout } = authSlice.actions;
export default authSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

budgetSlice.ts

This file manages budget state.

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { createBudget, getBudgets } from "../services/budgetService";

interface BudgetState {
  budgets: any[];
  loading: boolean;
  error: string | null;
}

const initialState: BudgetState = {
  budgets: [],
  loading: false,
  error: null,
};

export const fetchBudgets = createAsyncThunk("budgets/fetch", async () => {
  const response = await getBudgets();
  return response;
});

export const addBudget = createAsyncThunk(
  "budgets/add",
  async (budget: any, thunkAPI) => {
    try {
      const response = await createBudget(budget);
      return response;
    } catch (error) {
      return thunkAPI.rejectWithValue("Failed to create budget");
    }
  }
);

const budgetSlice = createSlice({
  name: "budgets",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchBudgets.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchBudgets.fulfilled, (state, action) => {
        state.budgets = action.payload;
        state.loading = false;
      })
      .addCase(fetchBudgets.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      });
  },
});

export default budgetSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

3. Styles

globals.css

This is the global CSS file where Tailwind CSS can be imported.

@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom global styles */
body {
  font-family: 'Inter', sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

tailwind.config.js

This file configures Tailwind CSS.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

4. Utility Functions

formatDate.ts

Helper function to format dates.

export const formatDate = (date: string) => {
  const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
  return new Date(date).toLocaleDateString(undefined, options);
};
Enter fullscreen mode Exit fullscreen mode

validateForm.ts

Helper function for form validation.

export const validateForm = (fields: { [key: string]: string }) => {
  const errors: { [key: string]: string } = {};

  for (const key in fields) {
    if (!fields[key]) {
      errors[key] = `${key} is required`;
    }
  }

  return errors;
};
Enter fullscreen mode Exit fullscreen mode

constants.ts

Application-wide constants.

export const BUDGET_CATEGORIES = [
  "Groceries",
  "Entertainment",
  "Transportation",
  "Housing",
  "Healthcare",
  "Savings",
];
Enter fullscreen mode Exit fullscreen mode

5. Types

transaction.ts

Types related to transactions.

export interface Transaction {
  id: string;
  description: string;
  amount: number;
  date: string;
  type: "income" | "expense";
}
Enter fullscreen mode Exit fullscreen mode

budget.ts

Types related to budgets.

export interface Budget {
  id: string;
  category: string;
  amount: number;
  startDate: string;
  endDate: string;
}
Enter fullscreen mode Exit fullscreen mode

user.ts

Types related to user and authentication.

export interface User {
  id: string;
  email: string;
  token: string;
}
Enter fullscreen mode Exit fullscreen mode

Summary

  • Services handle API requests for authentication, transactions, budgets, and reports.
  • Redux slices manage application state for authentication and budgets.
  • Styles include global CSS and Tailwind configuration for styling.
  • Utility functions help with tasks like date formatting and form validation.
  • TypeScript types define the structure of transactions, budgets, and users.

This setup provides a solid structure for a scalable and maintainable frontend application. Let me know if you need any further details or customizations!

Here’s the full backend code implementation for your Personal Finance Tracker backend structure using NestJS and Prisma. This implementation will cover authentication, user management, and the Prisma schema setup.


1. Prisma Schema (prisma/schema.prisma)

This schema defines your database models for users and transactions.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Transaction {
  id        String   @id @default(uuid())
  description String
  amount    Float
  date      DateTime
  userId    String
  user      User      @relation(fields: [userId], references: [id])
  createdAt DateTime  @default(now())
}
Enter fullscreen mode Exit fullscreen mode

2. Prisma Seed (prisma/seed.ts)

This file seeds initial data into the database.

import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

async function main() {
  await prisma.user.create({
    data: {
      email: "admin@example.com",
      password: "password123", // In a real-world app, this should be hashed
    },
  });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
Enter fullscreen mode Exit fullscreen mode

3. Authentication Module (src/auth)

auth.controller.ts

Handles login functionality.

import { Controller, Post, Body, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { LoginDto } from "./dto/login.dto";

@Controller("auth")
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post("login")
  async login(@Body() loginDto: LoginDto) {
    const token = await this.authService.validateUser(loginDto);
    if (!token) {
      throw new UnauthorizedException("Invalid credentials");
    }
    return { token };
  }
}
Enter fullscreen mode Exit fullscreen mode

auth.service.ts

Handles authentication logic and token generation.

import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { PrismaService } from "../prisma/prisma.service";
import { LoginDto } from "./dto/login.dto";
import * as bcrypt from "bcrypt";

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService, private jwtService: JwtService) {}

  async validateUser(loginDto: LoginDto) {
    const { email, password } = loginDto;
    const user = await this.prisma.user.findUnique({ where: { email } });

    if (user && (await bcrypt.compare(password, user.password))) {
      const payload = { email: user.email, sub: user.id };
      return this.jwtService.sign(payload);
    }

    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

auth.module.ts

Defines the structure of the auth module.

import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { JwtStrategy } from "./jwt.strategy";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: "60m" },
    }),
    PrismaModule,
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

jwt.strategy.ts

JWT strategy to secure routes using Passport.js.

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}
Enter fullscreen mode Exit fullscreen mode

dto/login.dto.ts

Defines the data transfer object (DTO) for login.

export class LoginDto {
  email: string;
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

4. User Module (src/users)

users.controller.ts

Handles user registration and profile management.

import { Controller, Post, Body, Get, Param, UseGuards } from "@nestjs/common";
import { UsersService } from "./users.service";
import { CreateUserDto } from "./dto/create-user.dto";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";

@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post("register")
  async register(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @UseGuards(JwtAuthGuard)
  @Get(":id")
  async findOne(@Param("id") id: string) {
    return this.usersService.findOne(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

users.service.ts

Handles business logic for user creation and retrieval.

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CreateUserDto } from "./dto/create-user.dto";
import * as bcrypt from "bcrypt";

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async create(createUserDto: CreateUserDto) {
    const { email, password } = createUserDto;
    const hashedPassword = await bcrypt.hash(password, 10);

    return this.prisma.user.create({
      data: {
        email,
        password: hashedPassword,
      },
    });
  }

  async findOne(id: string) {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

users.module.ts

Defines the structure of the user module.

import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";
import { UsersController } from "./users.controller";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
  imports: [PrismaModule],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

dto/create-user.dto.ts

Defines the DTO for creating a user.

export class CreateUserDto {
  email: string;
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

5. Prisma Service (src/prisma/prisma.service.ts)

This file provides the Prisma Client and is used across different modules.

import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
Enter fullscreen mode Exit fullscreen mode

prisma.module.ts

This module provides the Prisma service to other modules.

import { Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
Enter fullscreen mode Exit fullscreen mode

6. JWT Auth Guard (src/auth/jwt-auth.guard.ts)

This guard secures endpoints by checking if the user is authenticated.

import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") implements CanActivate {
  canActivate(context: ExecutionContext) {
    return super.canActivate(context);
  }
}
Enter fullscreen mode Exit fullscreen mode

7. Environment Variables

Ensure you have a .env file with the following content:

DATABASE_URL=postgresql://user:password@localhost:5432/personal_finance_tracker
JWT_SECRET=your_jwt_secret
Enter fullscreen mode Exit fullscreen mode

8. App Module (src/app.module.ts)

This is the main module that ties all other modules together.

import { Module } from "@nestjs/common";
import { AuthModule } from "./auth/auth.module";
import { UsersModule } from "./users/users.module";
import { PrismaModule } from "./prisma/prisma.module";

@Module({
  imports: [AuthModule, UsersModule, PrismaModule],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

9. Main File (src/main.ts)

This is the entry point of the application.

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Summary

This setup includes:

  • Prisma ORM for database interactions.
  • Authentication module with JWT and login functionality.
  • **Users

** module for registration and user management.

  • DTOs for validating incoming data and ensuring clean API requests.
  • Prisma service to handle database interactions globally.

To set up this project:

  1. Create a PostgreSQL database.
  2. Run prisma migrate dev to generate the migration files.
  3. Run prisma db seed to seed initial data.
  4. Use npm run start to run the NestJS backend.

Let me know if you need any additional features!

Here is the full implementation for the Transactions and Budgets modules, including the controllers, services, DTOs, and entities.


1. Transactions Module (src/transactions)

transactions.controller.ts

This file handles all API endpoints related to transactions.

import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from "@nestjs/common";
import { TransactionsService } from "./transactions.service";
import { CreateTransactionDto } from "./dto/create-transaction.dto";
import { UpdateTransactionDto } from "./dto/update-transaction.dto";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";

@Controller("transactions")
@UseGuards(JwtAuthGuard)
export class TransactionsController {
  constructor(private readonly transactionsService: TransactionsService) {}

  @Post()
  create(@Body() createTransactionDto: CreateTransactionDto) {
    return this.transactionsService.create(createTransactionDto);
  }

  @Get()
  findAll() {
    return this.transactionsService.findAll();
  }

  @Get(":id")
  findOne(@Param("id") id: string) {
    return this.transactionsService.findOne(id);
  }

  @Patch(":id")
  update(@Param("id") id: string, @Body() updateTransactionDto: UpdateTransactionDto) {
    return this.transactionsService.update(id, updateTransactionDto);
  }

  @Delete(":id")
  remove(@Param("id") id: string) {
    return this.transactionsService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

transactions.service.ts

This file contains the business logic for transactions.

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CreateTransactionDto } from "./dto/create-transaction.dto";
import { UpdateTransactionDto } from "./dto/update-transaction.dto";

@Injectable()
export class TransactionsService {
  constructor(private prisma: PrismaService) {}

  async create(createTransactionDto: CreateTransactionDto) {
    return this.prisma.transaction.create({
      data: {
        ...createTransactionDto,
      },
    });
  }

  async findAll() {
    return this.prisma.transaction.findMany();
  }

  async findOne(id: string) {
    return this.prisma.transaction.findUnique({ where: { id } });
  }

  async update(id: string, updateTransactionDto: UpdateTransactionDto) {
    return this.prisma.transaction.update({
      where: { id },
      data: updateTransactionDto,
    });
  }

  async remove(id: string) {
    return this.prisma.transaction.delete({ where: { id } });
  }
}
Enter fullscreen mode Exit fullscreen mode

transactions.module.ts

Defines the structure of the transactions module.

import { Module } from "@nestjs/common";
import { TransactionsService } from "./transactions.service";
import { TransactionsController } from "./transactions.controller";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
  imports: [PrismaModule],
  controllers: [TransactionsController],
  providers: [TransactionsService],
})
export class TransactionsModule {}
Enter fullscreen mode Exit fullscreen mode

dto/create-transaction.dto.ts

Defines the DTO for creating a transaction.

export class CreateTransactionDto {
  description: string;
  amount: number;
  date: string;
  userId: string; // Foreign key referencing user
}
Enter fullscreen mode Exit fullscreen mode

dto/update-transaction.dto.ts

Defines the DTO for updating a transaction.

export class UpdateTransactionDto {
  description?: string;
  amount?: number;
  date?: string;
}
Enter fullscreen mode Exit fullscreen mode

entities/transaction.entity.ts

Defines the transaction entity.

import { User } from "../../users/entities/user.entity";

export class Transaction {
  id: string;
  description: string;
  amount: number;
  date: Date;
  userId: string;
  user: User;
}
Enter fullscreen mode Exit fullscreen mode

2. Budgets Module (src/budgets)

budgets.controller.ts

Handles all API endpoints related to budgets.

import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from "@nestjs/common";
import { BudgetsService } from "./budgets.service";
import { CreateBudgetDto } from "./dto/create-budget.dto";
import { UpdateBudgetDto } from "./dto/update-budget.dto";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";

@Controller("budgets")
@UseGuards(JwtAuthGuard)
export class BudgetsController {
  constructor(private readonly budgetsService: BudgetsService) {}

  @Post()
  create(@Body() createBudgetDto: CreateBudgetDto) {
    return this.budgetsService.create(createBudgetDto);
  }

  @Get()
  findAll() {
    return this.budgetsService.findAll();
  }

  @Get(":id")
  findOne(@Param("id") id: string) {
    return this.budgetsService.findOne(id);
  }

  @Patch(":id")
  update(@Param("id") id: string, @Body() updateBudgetDto: UpdateBudgetDto) {
    return this.budgetsService.update(id, updateBudgetDto);
  }

  @Delete(":id")
  remove(@Param("id") id: string) {
    return this.budgetsService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

budgets.service.ts

Contains business logic for budgets.

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CreateBudgetDto } from "./dto/create-budget.dto";
import { UpdateBudgetDto } from "./dto/update-budget.dto";

@Injectable()
export class BudgetsService {
  constructor(private prisma: PrismaService) {}

  async create(createBudgetDto: CreateBudgetDto) {
    return this.prisma.budget.create({
      data: {
        ...createBudgetDto,
      },
    });
  }

  async findAll() {
    return this.prisma.budget.findMany();
  }

  async findOne(id: string) {
    return this.prisma.budget.findUnique({ where: { id } });
  }

  async update(id: string, updateBudgetDto: UpdateBudgetDto) {
    return this.prisma.budget.update({
      where: { id },
      data: updateBudgetDto,
    });
  }

  async remove(id: string) {
    return this.prisma.budget.delete({ where: { id } });
  }
}
Enter fullscreen mode Exit fullscreen mode

budgets.module.ts

Defines the structure of the budgets module.

import { Module } from "@nestjs/common";
import { BudgetsService } from "./budgets.service";
import { BudgetsController } from "./budgets.controller";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
  imports: [PrismaModule],
  controllers: [BudgetsController],
  providers: [BudgetsService],
})
export class BudgetsModule {}
Enter fullscreen mode Exit fullscreen mode

dto/create-budget.dto.ts

Defines the DTO for creating a budget.

export class CreateBudgetDto {
  category: string;
  amount: number;
  startDate: string;
  endDate: string;
  userId: string; // Foreign key referencing user
}
Enter fullscreen mode Exit fullscreen mode

dto/update-budget.dto.ts

Defines the DTO for updating a budget.

export class UpdateBudgetDto {
  category?: string;
  amount?: number;
  startDate?: string;
  endDate?: string;
}
Enter fullscreen mode Exit fullscreen mode

entities/budget.entity.ts

Defines the budget entity.

import { User } from "../../users/entities/user.entity";

export class Budget {
  id: string;
  category: string;
  amount: number;
  startDate: Date;
  endDate: Date;
  userId: string;
  user: User;
}
Enter fullscreen mode Exit fullscreen mode

3. Prisma Schema Update (prisma/schema.prisma)

Make sure your Prisma schema reflects the Transaction and Budget models.

model Transaction {
  id          String   @id @default(uuid())
  description String
  amount      Float
  date        DateTime
  userId      String
  user        User      @relation(fields: [userId], references: [id])
  createdAt   DateTime  @default(now())
}

model Budget {
  id        String   @id @default(uuid())
  category  String
  amount    Float
  startDate DateTime
  endDate   DateTime
  userId    String
  user      User      @relation(fields: [userId], references: [id])
  createdAt DateTime  @default(now())
}

model User {
  id          String         @id @default(uuid())
  email       String         @unique
  password    String
  createdAt   DateTime       @default(now())
  updatedAt   DateTime       @updatedAt
  transactions Transaction[]
  budgets      Budget[]
}
Enter fullscreen mode Exit fullscreen mode

Summary

This backend implementation for transactions and budgets includes:

  • API controllers: To handle HTTP requests for CRUD operations on transactions and budgets.
  • Services: Contains business logic for creating, updating, deleting, and retrieving data from the database.
  • DTOs: To validate and transfer data between API requests and the database.
  • Entities: Represent the structure of the data in the database.

To run this project:

  1. Update your Prisma schema and run prisma migrate dev to generate the migrations.
  2. Use npm run start to run the NestJS backend.

Let me know if you need more customizations or additional features!

Here is the full code implementation for the Reports and Financial Goals modules, including controllers, services, DTOs, and entities.


1. Reports Module (src/reports)

reports.controller.ts

This controller handles API requests for generating reports.

import { Controller, Get, Query, UseGuards } from "@nestjs/common";
import { ReportsService } from "./reports.service";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";

@Controller("reports")
@UseGuards(JwtAuthGuard)
export class ReportsController {
  constructor(private readonly reportsService: ReportsService) {}

  @Get("summary")
  async getSummaryReport(@Query("userId") userId: string) {
    return this.reportsService.generateSummaryReport(userId);
  }

  @Get("monthly")
  async getMonthlyReport(@Query("userId") userId: string, @Query("month") month: string) {
    return this.reportsService.generateMonthlyReport(userId, month);
  }
}
Enter fullscreen mode Exit fullscreen mode

reports.service.ts

This service handles the business logic for generating reports.

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";

@Injectable()
export class ReportsService {
  constructor(private prisma: PrismaService) {}

  async generateSummaryReport(userId: string) {
    const totalIncome = await this.prisma.transaction.aggregate({
      where: { userId, amount: { gt: 0 } },
      _sum: { amount: true },
    });

    const totalExpenses = await this.prisma.transaction.aggregate({
      where: { userId, amount: { lt: 0 } },
      _sum: { amount: true },
    });

    return {
      totalIncome: totalIncome._sum.amount || 0,
      totalExpenses: totalExpenses._sum.amount || 0,
      netIncome: (totalIncome._sum.amount || 0) + (totalExpenses._sum.amount || 0),
    };
  }

  async generateMonthlyReport(userId: string, month: string) {
    const startDate = new Date(`${month}-01`);
    const endDate = new Date(`${month}-31`);

    const monthlyIncome = await this.prisma.transaction.aggregate({
      where: {
        userId,
        amount: { gt: 0 },
        date: { gte: startDate, lte: endDate },
      },
      _sum: { amount: true },
    });

    const monthlyExpenses = await this.prisma.transaction.aggregate({
      where: {
        userId,
        amount: { lt: 0 },
        date: { gte: startDate, lte: endDate },
      },
      _sum: { amount: true },
    });

    return {
      monthlyIncome: monthlyIncome._sum.amount || 0,
      monthlyExpenses: monthlyExpenses._sum.amount || 0,
      netIncome: (monthlyIncome._sum.amount || 0) + (monthlyExpenses._sum.amount || 0),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

reports.module.ts

This file defines the structure of the reports module.

import { Module } from "@nestjs/common";
import { ReportsService } from "./reports.service";
import { ReportsController } from "./reports.controller";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
  imports: [PrismaModule],
  controllers: [ReportsController],
  providers: [ReportsService],
})
export class ReportsModule {}
Enter fullscreen mode Exit fullscreen mode

2. Financial Goals Module (src/goals)

goals.controller.ts

This controller handles API requests related to financial goals.

import { Controller, Get, Post, Patch, Delete, Param, Body, UseGuards } from "@nestjs/common";
import { GoalsService } from "./goals.service";
import { CreateGoalDto } from "./dto/create-goal.dto";
import { UpdateGoalDto } from "./dto/update-goal.dto";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";

@Controller("goals")
@UseGuards(JwtAuthGuard)
export class GoalsController {
  constructor(private readonly goalsService: GoalsService) {}

  @Post()
  async create(@Body() createGoalDto: CreateGoalDto) {
    return this.goalsService.create(createGoalDto);
  }

  @Get()
  async findAll(@Param("userId") userId: string) {
    return this.goalsService.findAll(userId);
  }

  @Patch(":id")
  async update(@Param("id") id: string, @Body() updateGoalDto: UpdateGoalDto) {
    return this.goalsService.update(id, updateGoalDto);
  }

  @Delete(":id")
  async remove(@Param("id") id: string) {
    return this.goalsService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

goals.service.ts

This service handles the business logic for managing financial goals.

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CreateGoalDto } from "./dto/create-goal.dto";
import { UpdateGoalDto } from "./dto/update-goal.dto";

@Injectable()
export class GoalsService {
  constructor(private prisma: PrismaService) {}

  async create(createGoalDto: CreateGoalDto) {
    return this.prisma.goal.create({
      data: {
        ...createGoalDto,
      },
    });
  }

  async findAll(userId: string) {
    return this.prisma.goal.findMany({
      where: { userId },
    });
  }

  async update(id: string, updateGoalDto: UpdateGoalDto) {
    return this.prisma.goal.update({
      where: { id },
      data: updateGoalDto,
    });
  }

  async remove(id: string) {
    return this.prisma.goal.delete({
      where: { id },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

goals.module.ts

This file defines the structure of the financial goals module.

import { Module } from "@nestjs/common";
import { GoalsService } from "./goals.service";
import { GoalsController } from "./goals.controller";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
  imports: [PrismaModule],
  controllers: [GoalsController],
  providers: [GoalsService],
})
export class GoalsModule {}
Enter fullscreen mode Exit fullscreen mode

dto/create-goal.dto.ts

Defines the DTO for creating a financial goal.

export class CreateGoalDto {
  userId: string; // Foreign key referencing user
  name: string;
  targetAmount: number;
  currentAmount: number;
  deadline: Date;
}
Enter fullscreen mode Exit fullscreen mode

dto/update-goal.dto.ts

Defines the DTO for updating a financial goal.

export class UpdateGoalDto {
  name?: string;
  targetAmount?: number;
  currentAmount?: number;
  deadline?: Date;
}
Enter fullscreen mode Exit fullscreen mode

entities/goal.entity.ts

Defines the goal entity.

import { User } from "../../users/entities/user.entity";

export class Goal {
  id: string;
  name: string;
  targetAmount: number;
  currentAmount: number;
  deadline: Date;
  userId: string;
  user: User;
}
Enter fullscreen mode Exit fullscreen mode

3. Prisma Schema Update (prisma/schema.prisma)

Ensure your Prisma schema reflects the Goal model.

model Goal {
  id            String   @id @default(uuid())
  name          String
  targetAmount  Float
  currentAmount Float
  deadline      DateTime
  userId        String
  user          User      @relation(fields: [userId], references: [id])
  createdAt     DateTime  @default(now())
}

model User {
  id          String    @id @default(uuid())
  email       String    @unique
  password    String
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  transactions Transaction[]
  budgets      Budget[]
  goals        Goal[]
}
Enter fullscreen mode Exit fullscreen mode

After updating the schema, make sure to run:

npx prisma migrate dev --name add-goals
Enter fullscreen mode Exit fullscreen mode

Summary

This implementation includes:

  • Reports module with endpoints for generating summary and monthly reports.
  • Financial goals module with CRUD operations for managing user financial goals.
  • DTOs to handle validation and structuring of data between API requests and the database.
  • Entities that represent data models for financial goals and reports.

To run this project:

  1. Update your Prisma schema and run npx prisma migrate dev to apply the new database migrations.
  2. Use npm run start to start the NestJS backend.

Let me know if you need more details or further assistance!

Here’s the complete code implementation for the Notifications, Config, Common, and App Module in your NestJS backend.


1. Notifications Module (src/notifications)

notifications.controller.ts

This file handles API endpoints for sending notifications.

import { Controller, Post, Body, UseGuards } from "@nestjs/common";
import { NotificationsService } from "./notifications.service";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";

@Controller("notifications")
@UseGuards(JwtAuthGuard)
export class NotificationsController {
  constructor(private readonly notificationsService: NotificationsService) {}

  @Post("send")
  async sendNotification(@Body() { userId, message }: { userId: string; message: string }) {
    return this.notificationsService.sendNotification(userId, message);
  }
}
Enter fullscreen mode Exit fullscreen mode

notifications.service.ts

This file contains business logic for sending notifications. You can expand this to support email, SMS, or push notifications.

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";

@Injectable()
export class NotificationsService {
  constructor(private prisma: PrismaService) {}

  async sendNotification(userId: string, message: string) {
    // Mock notification logic
    console.log(`Notification sent to user ${userId}: ${message}`);

    // Log the notification in the database
    return this.prisma.notification.create({
      data: {
        userId,
        message,
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

notifications.module.ts

This file defines the structure of the notifications module.

import { Module } from "@nestjs/common";
import { NotificationsService } from "./notifications.service";
import { NotificationsController } from "./notifications.controller";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
  imports: [PrismaModule],
  controllers: [NotificationsController],
  providers: [NotificationsService],
})
export class NotificationsModule {}
Enter fullscreen mode Exit fullscreen mode

2. Config Module (src/config)

config.module.ts

This module provides a centralized configuration service to access environment variables.

import { Module } from "@nestjs/common";
import { ConfigService } from "./config.service";

@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}
Enter fullscreen mode Exit fullscreen mode

config.service.ts

This service provides a way to access environment variables.

import { Injectable } from "@nestjs/common";
import * as dotenv from "dotenv";

dotenv.config();

@Injectable()
export class ConfigService {
  get(key: string): string {
    return process.env[key];
  }

  getDatabaseUrl(): string {
    return this.get("DATABASE_URL");
  }

  getJwtSecret(): string {
    return this.get("JWT_SECRET");
  }

  getPort(): number {
    return parseInt(this.get("PORT")) || 3000;
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Common Module (src/common)

decorators/roles.decorator.ts

This file provides a role-based access control decorator.

import { SetMetadata } from "@nestjs/common";

export const ROLES_KEY = "roles";
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
Enter fullscreen mode Exit fullscreen mode

filters/all-exception.filter.ts

This file handles global exception filtering and can be used for better error handling across the application.

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from "@nestjs/common";
import { Request, Response } from "express";

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException ? exception.getResponse() : "Internal server error";

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Root App Module (src/app.module.ts)

The App Module ties everything together, importing all the modules.

import { Module } from "@nestjs/common";
import { AuthModule } from "./auth/auth.module";
import { UsersModule } from "./users/users.module";
import { PrismaModule } from "./prisma/prisma.module";
import { TransactionsModule } from "./transactions/transactions.module";
import { BudgetsModule } from "./budgets/budgets.module";
import { ReportsModule } from "./reports/reports.module";
import { GoalsModule } from "./goals/goals.module";
import { NotificationsModule } from "./notifications/notifications.module";
import { ConfigModule } from "./config/config.module";
import { APP_FILTER } from "@nestjs/core";
import { AllExceptionsFilter } from "./common/filters/all-exception.filter";

@Module({
  imports: [
    AuthModule,
    UsersModule,
    PrismaModule,
    TransactionsModule,
    BudgetsModule,
    ReportsModule,
    GoalsModule,
    NotificationsModule,
    ConfigModule,
  ],
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

5. Main Entry Point (src/main.ts)

This is the entry point of the application. It starts the server and applies global configurations like exception filters and validation pipes.

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { AllExceptionsFilter } from "./common/filters/all-exception.filter";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalFilters(new AllExceptionsFilter());

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

6. Notification Entity (Optional)

If you want to store notifications in the database, here’s an optional notification entity for Prisma.

Update schema.prisma

Add this to your Prisma schema:

model Notification {
  id        String   @id @default(uuid())
  userId    String
  user      User     @relation(fields: [userId], references: [id])
  message   String
  createdAt DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

After updating the schema, run:

npx prisma migrate dev --name add-notifications
Enter fullscreen mode Exit fullscreen mode

Summary

  • Notifications Module: Handles sending notifications and can be extended to support various notification channels (e.g., email, SMS, push).
  • Config Module: Centralizes environment variable management.
  • Common Module: Includes a global exception filter and role-based access decorators.
  • App Module: Imports all necessary modules and applies global settings.
  • Main Entry Point: Configures the application, initializes global filters and validation, and starts the server.

This structure provides a solid foundation for a maintainable and scalable NestJS backend. Let me know if you need further details or modifications!

If you enjoy my content and would like to support my work, you can buy me a coffee. Your support is greatly appreciated!

Disclaimer: This content is generated by AI.

Top comments (0)