Core Source Code & Receipt Processing Implementation
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.
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> ); };
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 : [], }; }
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]; }
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> ); };