← Back to Project Overview

💳 Expense Tracker AI

Core Source Code & Receipt Processing Implementation

React 19.1+ TypeScript Gemini AI Recharts

📋 About This Code Showcase

This page presents the core implementation of the Expense Tracker AI, demonstrating how modern React, TypeScript, and Google Gemini AI work together to create an intelligent expense management system. The application showcases advanced patterns in AI integration, local storage management, and real-time data visualization.

🎯 Key Implementation Highlights:
  • AI-powered receipt processing with Google Gemini vision API
  • Type-safe React components with comprehensive TypeScript definitions
  • Custom hooks for local storage persistence and state management
  • Interactive data visualization with Recharts integration
  • Modern React patterns with functional components and hooks
📱 App.tsx - Main Application Component
import React, { useState } from 'react';
import { useLocalStorage } from './hooks/useLocalStorage';
import { Expense, ExpenseData } from './types';
import Dashboard from './components/Dashboard';
import ReceiptProcessorModal from './components/ReceiptProcessorModal';

const App: React.FC = () => {
  // Local storage integration for expense persistence
  const [expenses, setExpenses] = useLocalStorage<Expense[]>('expenses', []);
  const [isModalOpen, setIsModalOpen] = useState(false);

  // Handle new expense addition with automatic sorting
  const handleAddExpense = (expenseData: ExpenseData) => {
    const newExpense: Expense = {
      ...expenseData,
      id: `${Date.now()}-${Math.random()}`,
      date: new Date(expenseData.date).toISOString(),
    };

    setExpenses(prevExpenses =>
      [...prevExpenses, newExpense].sort((a, b) =>
        new Date(b.date).getTime() - new Date(a.date).getTime()
      )
    );
    setIsModalOpen(false);
  };

  return (
    <div className="min-h-screen bg-base-100">
      <header className="bg-base-200/50 backdrop-blur-sm">
        <h1>Expense Tracker <span className="text-brand-primary">AI</span></h1>
      </header>
      <main>
        <Dashboard expenses={expenses} />
        <ExpenseList expenses={expenses.slice(0, 10)} />
      </main>
      <ReceiptProcessorModal
        isOpen={isModalOpen}
        onSave={handleAddExpense}
      />
    </div>
  );
};
            
🤖 geminiService.ts - AI Receipt Processing
import { GoogleGenerativeAI } from '@google/generative-ai';
import { ExpenseData } from '../types';

// Initialize Gemini AI with API key
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');

export async function extractExpenseDataFromImage(imageFile: File): Promise<ExpenseData> {
  try {
    // Convert image to base64 for AI processing
    const base64Data = await fileToBase64(imageFile);
    const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });

    // Structured prompt for expense data extraction
    const prompt = `
      Analyze this receipt image and extract the following information in JSON format:
      {
        "merchant": "Store/Restaurant name",
        "date": "YYYY-MM-DD format",
        "total": number (total amount),
        "currency": "USD or detected currency",
        "category": "Food|Transport|Utilities|Entertainment|Shopping|Health|Housing|Other",
        "items": [
          {"name": "Item description", "price": number}
        ]
      }

      Rules:
      - Extract exact merchant name from receipt
      - Parse date accurately (today if unclear: ${new Date().toISOString().split('T')[0]})
      - Include ALL line items with prices
      - Suggest most appropriate category
      - Return only valid JSON, no additional text
    `;

    const result = await model.generateContent([
      prompt,
      {
        inlineData: {
          data: base64Data,
          mimeType: imageFile.type,
        },
      },
    ]);

    // Parse and validate AI response
    const response = await result.response;
    const text = response.text();
    const cleanedText = text.replace(/```json\n?|\n?```/g, '').trim();

    const extractedData = JSON.parse(cleanedText) as ExpenseData;

    // Validate and sanitize extracted data
    return validateExpenseData(extractedData);

  } catch (error) {
    console.error('AI extraction failed:', error);
    throw new Error('Failed to process receipt. Please try again.');
  }
}

// Data validation and sanitization
function validateExpenseData(data: any): ExpenseData {
  return {
    merchant: data.merchant || 'Unknown Merchant',
    date: data.date || new Date().toISOString().split('T')[0],
    total: Number(data.total) || 0,
    currency: data.currency || 'USD',
    category: data.category || 'Other',
    items: Array.isArray(data.items) ? data.items : [],
  };
}
            
🎣 useLocalStorage.ts - Custom Hook for Data Persistence
import { useState, useEffect } from 'react';

// Generic hook for localStorage with TypeScript support
export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {

  // Initialize state with localStorage value or default
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      if (typeof window === 'undefined') {
        return initialValue;
      }

      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Update localStorage when state changes
  const setValue = (value: T | ((prev: T) => T)) => {
    try {
      const valueToStore = value instanceof Function
        ? value(storedValue)
        : value;

      setStoredValue(valueToStore);

      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue];
}
            
📊 Dashboard.tsx - Analytics Component
import React, { useState, useMemo } from 'react';
import { BarChart, Bar, XAxis, YAxis, PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
import { format, subDays, startOfWeek, startOfMonth, startOfYear } from 'date-fns';
import { Expense, TimeView } from '../types';

interface DashboardProps {
  expenses: Expense[];
}

const Dashboard: React.FC<DashboardProps> = ({ expenses }) => {
  const [timeView, setTimeView] = useState<TimeView>('Monthly');

  // Memoized analytics calculations for performance
  const analytics = useMemo(() => {
    const now = new Date();
    let startDate: Date;

    // Dynamic date range based on selected view
    switch (timeView) {
      case 'Daily':
        startDate = subDays(now, 7);
        break;
      case 'Weekly':
        startDate = startOfWeek(subDays(now, 28));
        break;
      case 'Monthly':
        startDate = startOfMonth(subDays(now, 90));
        break;
      case 'Yearly':
        startDate = startOfYear(subDays(now, 365));
        break;
    }

    // Filter and aggregate expense data
    const filteredExpenses = expenses.filter(expense =>
      new Date(expense.date) >= startDate
    );

    // Category-based spending analysis
    const categoryTotals = filteredExpenses.reduce((acc, expense) => {
      acc[expense.category] = (acc[expense.category] || 0) + expense.total;
      return acc;
    }, {} as Record<string, number>);

    return {
      totalSpent: filteredExpenses.reduce((sum, exp) => sum + exp.total, 0),
      categoryData: Object.entries(categoryTotals).map(([name, value]) => ({
        name,
        value,
        percentage: ((value / filteredExpenses.reduce((sum, exp) => sum + exp.total, 0)) * 100).toFixed(1)
      }))
    };
  }, [expenses, timeView]);

  return (
    <div className="dashboard-container">
      <div className="time-selector">
        {['Daily', 'Weekly', 'Monthly', 'Yearly'].map(view => (
          <button
            key={view}
            onClick={() => setTimeView(view as TimeView)}
            className={timeView === view ? 'active' : ''}
          >
            {view}
          </button>
        ))}
      </div>

      <div className="charts-grid">
        <ResponsiveContainer width="100%" height={300}>
          <PieChart>
            <Pie
              data={analytics.categoryData}
              dataKey="value"
              nameKey="name"
              cx="50%"
              cy="50%"
              label={({ name, percentage }) => `${name}: ${percentage}%`}
            />
          </PieChart>
        </ResponsiveContainer>
      </div>
    </div>
  );
};
            

🔧 Technical Implementation Notes

🎯 Architecture Decisions

🚀 Performance Optimizations

🛡️ Security & Privacy Features

← Back to Project Overview | Portfolio Home