import React, { useState, useEffect, useContext, createContext } from 'react';
import { Home, Upload, List, ShoppingCart, TrendingUp, Calendar, Trash2, Edit2, Save, X } from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, BarChart, Bar, PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
const StoreContext = createContext();
const StoreProvider = ({ children }) => {
const [products, setProducts] = useState(() => {
const stored = localStorage.getItem('grocery-tracker-storage');
return stored ? JSON.parse(stored) : [];
});
useEffect(() => {
localStorage.setItem('grocery-tracker-storage', JSON.stringify(products));
}, [products]);
const addProduct = (product) => {
setProducts(prev => [...prev, { ...product, id: Date.now() }]);
};
const updateProduct = (id, product) => {
setProducts(prev => prev.map(p => p.id === id ? { ...p, ...product } : p));
};
const deleteProduct = (id) => {
setProducts(prev => prev.filter(p => p.id !== id));
};
const clearProducts = () => {
setProducts([]);
};
return (
{children}
);
};
const useStore = () => {
const context = useContext(StoreContext);
if (!context) {
throw new Error('useStore must be used within a StoreProvider');
}
return context;
};
const MISTRAL_API_KEY = process.env.REACT_APP_MISTRAL_API_KEY;
const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions';
const convertImageToBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
const extractTextFromImage = async (file, userApiKey) => {
const apiKeyToUse = userApiKey || MISTRAL_API_KEY;
try {
const base64Image = await convertImageToBase64(file);
const response = await fetch(MISTRAL_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKeyToUse}`
},
body: JSON.stringify({
model: 'pixtral-12b-2409',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: 'Extract all text from this receipt image. Please provide the raw text exactly as it appears, maintaining the original structure and formatting.'
},
{
type: 'image_url',
image_url: {
url: base64Image
}
}
]
}
],
max_tokens: 1000,
temperature: 0.1
})
});
if (!response.ok) {
throw new Error(`Mistral API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
console.error('Error extracting text from image:', error);
throw new Error('Failed to extract text from image');
}
};
const processReceiptText = async (text, userApiKey) => {
const apiKeyToUse = userApiKey || MISTRAL_API_KEY;
try {
const response = await fetch(MISTRAL_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKeyToUse}`
},
body: JSON.stringify({
model: 'mistral-large-latest',
messages: [
{
role: 'user',
content: `Analyze this receipt text and extract product information. Return a JSON array with objects containing: name, price, store (with name and address), and date.
Receipt text:
${text}
Please return ONLY a valid JSON array in this format:
[
{
"name": "Product Name",
"price": 2.50,
"store": {
"name": "Store Name",
"address": "Store Address"
},
"date": "MM/DD/YYYY"
}
]`
}
],
max_tokens: 2000,
temperature: 0.1
})
});
if (!response.ok) {
throw new Error(`Mistral API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const jsonResponse = data.choices[0].message.content;
const jsonMatch = jsonResponse.match(/\[[\s\S]*\]/);
if (!jsonMatch) {
throw new Error('Invalid JSON response from Mistral');
}
const products = JSON.parse(jsonMatch[0]);
return products.map(product => ({
...product,
price: parseFloat(product.price) || 0
}));
} catch (error) {
console.error('Error processing receipt text:', error);
throw new Error('Failed to process receipt text');
}
};
const UploadTab = () => {
const { addProduct } = useStore();
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const [step, setStep] = useState('upload');
const [extractedText, setExtractedText] = useState('');
const [processedProducts, setProcessedProducts] = useState([]);
const [error, setError] = useState(null);
const [apiKey, setApiKey] = useState('');
const handleFileSelect = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile && selectedFile.type.startsWith('image/')) {
setFile(selectedFile);
setStep('upload');
setError(null);
}
};
const handleUpload = async () => {
if (!file) return;
if (!apiKey && !MISTRAL_API_KEY) {
setError('Please enter your Mistral API key or configure REACT_APP_MISTRAL_API_KEY environment variable.');
return;
}
setLoading(true);
setError(null);
setStep('extracting');
try {
const text = await extractTextFromImage(file, apiKey);
setExtractedText(text);
setStep('processing');
const products = await processReceiptText(text, apiKey);
setProcessedProducts(products);
setStep('review');
} catch (error) {
console.error('Error processing receipt:', error);
setError(error.message);
setStep('upload');
} finally {
setLoading(false);
}
};
const handleSaveProducts = () => {
processedProducts.forEach(product => addProduct(product));
setFile(null);
setExtractedText('');
setProcessedProducts([]);
setStep('upload');
setError(null);
alert('Products saved successfully');
};
return (
Upload Receipt
{error && (
)}
{step === 'upload' && (
{file && (
Selected file: {file.name}
)}
{file && (
)}
)}
{step === 'extracting' && (
Extracting text from image...
)}
{step === 'processing' && (
)}
{step === 'review' && (
Detected Products
Extracted Text:
{extractedText}
{processedProducts.length > 0 ? (
processedProducts.map((product, index) => (
{product.name}
({product.store?.name || 'Unknown Store'})
${product.price.toFixed(2)}
))
) : (
No products detected
)}
)}
);
};
const Sidebar = ({ activeTab, setActiveTab }) => {
const tabs = [
{ id: 'home', label: 'Home', icon: Home },
{ id: 'upload', label: 'Upload', icon: Upload },
{ id: 'list', label: 'List', icon: List }
];
return (
Grocery Tracker
);
};
const HomeTab = () => {
const { products } = useStore();
const totalSpent = products.reduce((sum, p) => sum + p.price, 0);
const productsByStore = products.reduce((acc, p) => {
const key = p.store.name;
if (!acc[key]) acc[key] = [];
acc[key].push(p);
return acc;
}, {});
const priceVariations = products.reduce((acc, p) => {
if (!acc[p.name]) acc[p.name] = [];
acc[p.name].push(p);
return acc;
}, {});
const productsWithVariation = Object.entries(priceVariations)
.filter(([_, prods]) => prods.length > 1)
.map(([name, prods]) => ({
name,
minPrice: Math.min(...prods.map(p => p.price)),
maxPrice: Math.max(...prods.map(p => p.price)),
difference: Math.max(...prods.map(p => p.price)) - Math.min(...prods.map(p => p.price))
}))
.sort((a, b) => b.difference - a.difference)
.slice(0, 5);
const spendingByStore = Object.entries(productsByStore).map(([name, prods]) => ({
name,
spending: prods.reduce((sum, p) => sum + p.price, 0),
products: prods.length
}));
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
return (
Dashboard
Total Spent
${totalSpent.toFixed(2)}
Registered Products
{products.length}
Stores
{Object.keys(productsByStore).length}
{spendingByStore.length > 0 && (
Spending by Store
[`$${value.toFixed(2)}`, 'Spending']} />
Spending Distribution
`${name} ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="spending"
>
{spendingByStore.map((entry, index) => (
|
))}
[`$${value.toFixed(2)}`, 'Spending']} />
)}
{productsWithVariation.length > 0 && (
Products with Highest Price Variation
{productsWithVariation.map((product, index) => (
{product.name}
${product.minPrice.toFixed(2)} - ${product.maxPrice.toFixed(2)}
+${product.difference.toFixed(2)}
))}
)}
);
};
const ListTab = () => {
const { products, updateProduct, deleteProduct } = useStore();
const [editingId, setEditingId] = useState(null);
const [editForm, setEditForm] = useState({});
const startEdit = (product) => {
setEditingId(product.id);
setEditForm(product);
};
const saveEdit = () => {
updateProduct(editingId, editForm);
setEditingId(null);
setEditForm({});
};
const cancelEdit = () => {
setEditingId(null);
setEditForm({});
};
const handleDelete = (id) => {
if (window.confirm('Are you sure you want to delete this product?')) {
deleteProduct(id);
}
};
return (
Product List
{products.length === 0 && (
)}
);
};
const App = () => {
const [activeTab, setActiveTab] = useState('home');
const renderContent = () => {
switch (activeTab) {
case 'home':
return ;
case 'upload':
return ;
case 'list':
return ;
default:
return ;
}
};
return (
{renderContent()}
);
};
export default App;